| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539 |
- {% extends "layout.html" %}
- {% block page_title %}教材管理{% endblock %}
- {% block content %}
- <!-- 目录节点树状结构宏定义 -->
- {% macro render_catalog_node(node, level) %}
- <div
- class="catalog-node relative"
- data-node-id="{{ node.id }}"
- data-node-type="{{ node.node_type }}"
- data-parent="{{ node.parent_id or '' }}"
- data-level="{{ level }}"
- data-textbook-id="{{ node.textbook_id }}">
-
- <!-- 节点卡片 -->
- <div class="flex items-start gap-4 group">
- <!-- 展开/折叠按钮区域 -->
- <div class="flex items-center gap-2 pt-4 flex-shrink-0">
- {% if node.children|length > 0 %}
- <button
- onclick="toggleCatalogNode('{{ node.id }}'); event.stopPropagation();"
- class="w-8 h-8 rounded-full bg-white border-2 border-gray-300 hover:border-blue-500 flex items-center justify-center text-gray-600 hover:text-blue-600 transition-all expand-btn z-10 shadow-sm hover:shadow-md"
- data-expanded="{% if level == 0 %}true{% else %}false{% endif %}">
- {% if level == 0 %}
- <i class="ri-subtract-line text-sm"></i>
- {% else %}
- <i class="ri-add-line text-sm"></i>
- {% endif %}
- </button>
- {% else %}
- <div class="w-8 h-8 flex items-center justify-center">
- <div class="w-2 h-2 rounded-full bg-gray-400"></div>
- </div>
- {% endif %}
- </div>
-
- <!-- 节点内容卡片 -->
- <div class="flex-1 apple-card p-5 hover:shadow-xl transition-all duration-300 group-hover:border-blue-300 border-2 border-transparent rounded-xl {% if node.children|length > 0 %}cursor-pointer{% endif %}" {% if node.children|length > 0 %}onclick="handleCatalogCardClick(event, '{{ node.id }}')"{% endif %}>
- <div class="flex items-start justify-between">
- <div class="flex-1 min-w-0">
- <div class="flex items-center gap-3 mb-3 flex-wrap">
- <span class="px-3 py-1.5 rounded-lg text-xs font-bold font-mono bg-gradient-to-r {% if node.node_type == 'chapter' %}from-blue-500 to-indigo-600{% elif node.node_type == 'section' %}from-green-500 to-emerald-600{% else %}from-orange-500 to-amber-600{% endif %} text-white shadow-md">
- {{ node.display_no or node.node_type }}
- </span>
- <h3 class="text-lg font-bold text-gray-800 group-hover:text-blue-600 transition-colors">
- {{ node.title }}
- </h3>
- </div>
-
- <div class="flex items-center gap-4 text-sm text-gray-500 mt-2 flex-wrap">
- <span class="flex items-center gap-1.5 px-2 py-1 bg-gray-50 rounded-md">
- <i class="ri-file-list-line text-blue-500"></i>
- <span>{{ node.node_type }}</span>
- </span>
- {% if node.depth %}
- <span class="flex items-center gap-1.5 px-2 py-1 bg-gray-50 rounded-md">
- <i class="ri-stack-line text-green-500"></i>
- <span>层级: {{ node.depth }}</span>
- </span>
- {% endif %}
- {% if node.children|length > 0 %}
- <span class="flex items-center gap-1.5 px-2 py-1 bg-blue-50 rounded-md text-blue-600">
- <i class="ri-node-tree"></i>
- <span class="font-semibold">{{ node.children|length }} 个子节点</span>
- </span>
- {% endif %}
- </div>
- </div>
-
- <!-- 操作按钮 -->
- <div class="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity ml-4 flex-shrink-0" onclick="event.stopPropagation()">
- {% if node.node_type == 'section' %}
- <button
- onclick="showAddKpRelationModal('{{ node.id }}', '{{ node.title }}')"
- class="px-3 py-1.5 text-xs font-semibold text-white bg-gradient-to-r from-purple-500 to-indigo-600 hover:from-purple-600 hover:to-indigo-700 rounded-lg transition-all shadow-sm hover:shadow-md flex items-center gap-1.5"
- title="关联知识点">
- <i class="ri-link"></i>
- <span>关联知识点</span>
- </button>
- {% endif %}
- <button
- onclick="showAddChildCatalogModal('{{ node.id }}', '{{ node.title }}', '{{ node.node_type }}')"
- class="px-3 py-1.5 text-xs font-semibold text-white bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 rounded-lg transition-all shadow-sm hover:shadow-md flex items-center gap-1.5"
- title="添加子节点">
- <i class="ri-add-line"></i>
- <span>添加子节点</span>
- </button>
- <button
- onclick="showEditCatalogModal({{ node.id }})"
- class="w-9 h-9 rounded-lg text-blue-600 bg-blue-50 hover:bg-blue-100 flex items-center justify-center transition-colors shadow-sm hover:shadow-md"
- title="编辑">
- <i class="ri-edit-line"></i>
- </button>
- <button
- onclick="showDeleteCatalogConfirm({{ node.id }}, '{{ node.title }}')"
- class="w-9 h-9 rounded-lg text-red-600 bg-red-50 hover:bg-red-100 flex items-center justify-center transition-colors shadow-sm hover:shadow-md"
- title="删除">
- <i class="ri-delete-bin-line"></i>
- </button>
- </div>
- </div>
- </div>
- </div>
-
- <!-- 子节点容器 -->
- {% if node.children|length > 0 %}
- <div class="children-container mt-3 ml-12 {% if level >= 1 %}hidden{% endif %}" id="children-{{ node.id }}" data-level="{{ level }}">
- {% for child in node.children %}
- {{ render_catalog_node(child, level + 1) }}
- {% endfor %}
- </div>
- {% endif %}
- </div>
- {% endmacro %}
- <div class="flex gap-6">
- <!-- 左侧系列菜单 -->
- <div class="w-80 flex-shrink-0">
- <div class="apple-card p-6 sticky top-4 max-h-[calc(100vh-2rem)] overflow-y-auto">
- <div class="flex items-center justify-between mb-6 pb-3 border-b border-gray-200">
- <h2 class="text-lg font-bold text-gray-800">教材系列</h2>
- <button
- onclick="showAddSeriesModal()"
- class="btn-apple bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-700 hover:to-indigo-700 text-xs py-1.5 px-3 rounded-lg shadow-sm flex items-center gap-1">
- <i class="ri-add-circle-line"></i>
- <span>添加</span>
- </button>
- </div>
-
- <!-- 搜索框 -->
- <div class="mb-4">
- <div class="relative">
- <i class="ri-search-line absolute left-2.5 top-1/2 transform -translate-y-1/2 text-gray-400 text-xs"></i>
- <input
- type="text"
- id="seriesSearchInput"
- placeholder="搜索系列..."
- class="w-full pl-7 pr-3 py-2 text-xs border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
- oninput="filterSeriesList()">
- </div>
- </div>
-
- <!-- 系列列表 -->
- <nav class="space-y-1" id="seriesListNav">
- {% for series in series_list %}
- <div class="series-item mb-2" data-series-id="{{ series.id }}" data-series-name="{{ series.name }}" data-series-active="{{ series.is_active }}">
- <div class="flex items-center gap-2 group">
- <div class="flex-1 flex items-center gap-2">
- <a href="javascript:void(0)"
- onclick="switchSeries({{ series.id }})"
- class="flex-1 block px-4 py-2.5 rounded-xl hover:bg-gradient-to-r hover:from-slate-50 hover:to-blue-50 transition-all border-l-4 border-transparent hover:border-slate-500 series-link {% if loop.first %}bg-gradient-to-r from-slate-50 to-blue-50 border-slate-500{% endif %}">
- <span class="text-sm font-semibold text-gray-800 group-hover:text-slate-600 transition-colors">{{ series.name }}</span>
- </a>
- <!-- 激活状态开关 -->
- <label class="relative inline-flex items-center cursor-pointer flex-shrink-0" onclick="event.stopPropagation(); event.preventDefault(); toggleSeriesActiveDirect({{ series.id }}, this);">
- <input type="checkbox" class="sr-only peer" {% if series.is_active == 1 %}checked{% endif %} onclick="event.stopPropagation();">
- <div class="w-9 h-5 bg-gray-300 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-500 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-blue-600"></div>
- </label>
- </div>
- <button
- onclick="event.stopPropagation(); event.preventDefault(); showEditSeriesModal({{ series.id }});"
- class="opacity-0 group-hover:opacity-100 transition-opacity p-1.5 hover:bg-gray-100 rounded text-gray-500 hover:text-blue-600"
- title="编辑">
- <i class="ri-edit-line text-sm"></i>
- </button>
- </div>
- </div>
- {% endfor %}
- </nav>
- </div>
- </div>
-
- <!-- 右侧内容区域 -->
- <div class="flex-1 space-y-6">
- <!-- 教材列表(当前系列下的教材) -->
- <div id="textbookListContainer" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
- <!-- 教材卡片将通过JavaScript动态加载 -->
- </div>
- <!-- 目录树结构(选中教材后显示) -->
- <div id="catalogTreeContainer" class="hidden">
- <div class="flex items-center justify-between mb-4">
- <h2 class="text-xl font-bold text-gray-800" id="currentTextbookTitle"></h2>
- <button
- onclick="showAddCatalogModal()"
- class="btn-apple bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-700 hover:to-indigo-700 px-4 py-2 rounded-lg shadow-sm flex items-center gap-2">
- <i class="ri-add-circle-line"></i>
- <span>添加目录节点</span>
- </button>
- </div>
-
- <div class="space-y-3" id="catalogTree">
- <!-- 目录树将通过JavaScript动态加载 -->
- </div>
- </div>
- </div>
- </div>
- <!-- 添加/编辑教材系列模态框 -->
- <div id="seriesModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
- <div class="bg-white rounded-2xl shadow-2xl max-w-md w-full mx-4">
- <div class="p-6 border-b border-gray-200">
- <h2 id="seriesModalTitle" class="text-xl font-bold text-gray-800">添加教材系列</h2>
- </div>
- <form id="seriesForm" class="p-6 space-y-4">
- <input type="hidden" id="seriesId" name="id">
- <div>
- <label class="block text-sm font-semibold text-gray-700 mb-2">系列名称 *</label>
- <input type="text" id="seriesName" name="name" required
- class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
- </div>
- <div>
- <label class="block text-sm font-semibold text-gray-700 mb-2">标识符</label>
- <input type="text" id="seriesSlug" name="slug"
- class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
- </div>
- <div>
- <label class="flex items-center justify-between cursor-pointer">
- <span class="text-sm font-semibold text-gray-700">激活状态</span>
- <label class="relative inline-flex items-center cursor-pointer">
- <input type="checkbox" id="seriesIsActive" class="sr-only peer" checked>
- <div class="w-11 h-6 bg-gray-300 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-500 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
- </label>
- </label>
- <p class="text-xs text-gray-500 mt-1">激活的系列将在系统中可用</p>
- </div>
- <div class="flex gap-3 pt-4">
- <button type="button" onclick="closeSeriesModal()" class="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">取消</button>
- <button type="submit" class="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">保存</button>
- </div>
- </form>
- </div>
- </div>
- <!-- 添加/编辑教材模态框 -->
- <div id="textbookModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
- <div class="bg-white rounded-2xl shadow-2xl max-w-md w-full mx-4">
- <div class="p-6 border-b border-gray-200">
- <h2 id="textbookModalTitle" class="text-xl font-bold text-gray-800">添加教材</h2>
- </div>
- <form id="textbookForm" class="p-6 space-y-4">
- <input type="hidden" id="textbookId" name="id">
- <div>
- <label class="block text-sm font-semibold text-gray-700 mb-2">教材名称 *</label>
- <input type="text" id="textbookTitle" name="official_title" required
- class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
- </div>
- <div class="grid grid-cols-2 gap-4">
- <div>
- <label class="block text-sm font-semibold text-gray-700 mb-2">学段</label>
- <select id="textbookStage" name="stage" class="w-full px-4 py-2 border border-gray-300 rounded-lg">
- <option value="">请选择</option>
- <option value="primary">小学</option>
- <option value="junior">初中</option>
- <option value="senior">高中</option>
- </select>
- </div>
- <div>
- <label class="block text-sm font-semibold text-gray-700 mb-2">年级</label>
- <input type="text" id="textbookGrade" name="grade"
- class="w-full px-4 py-2 border border-gray-300 rounded-lg">
- </div>
- </div>
- <div>
- <label class="block text-sm font-semibold text-gray-700 mb-2">学期</label>
- <select id="textbookSemester" name="semester" class="w-full px-4 py-2 border border-gray-300 rounded-lg">
- <option value="">请选择</option>
- <option value="1">上学期</option>
- <option value="2">下学期</option>
- </select>
- </div>
- <div class="flex gap-3 pt-4">
- <button type="button" onclick="closeTextbookModal()" class="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">取消</button>
- <button type="submit" class="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">保存</button>
- </div>
- </form>
- </div>
- </div>
- <!-- 添加/编辑目录节点模态框 -->
- <div id="catalogModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
- <div class="bg-white rounded-2xl shadow-2xl max-w-md w-full mx-4">
- <div class="p-6 border-b border-gray-200">
- <h2 id="catalogModalTitle" class="text-xl font-bold text-gray-800">添加目录节点</h2>
- </div>
- <form id="catalogForm" class="p-6 space-y-4">
- <input type="hidden" id="catalogId" name="id">
- <input type="hidden" id="catalogTextbookId" name="textbook_id">
- <input type="hidden" id="catalogParentId" name="parent_id">
- <div>
- <label class="block text-sm font-semibold text-gray-700 mb-2">节点类型 *</label>
- <select id="catalogNodeType" name="node_type" required class="w-full px-4 py-2 border border-gray-300 rounded-lg">
- <option value="chapter">章节</option>
- <option value="section">小节</option>
- <option value="subsection">子小节</option>
- </select>
- </div>
- <div>
- <label class="block text-sm font-semibold text-gray-700 mb-2">标题 *</label>
- <input type="text" id="catalogTitle" name="title" required
- class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
- </div>
- <div>
- <label class="block text-sm font-semibold text-gray-700 mb-2">编号</label>
- <input type="text" id="catalogDisplayNo" name="display_no"
- class="w-full px-4 py-2 border border-gray-300 rounded-lg">
- </div>
- <div class="flex gap-3 pt-4">
- <button type="button" onclick="closeCatalogModal()" class="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">取消</button>
- <button type="submit" class="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">保存</button>
- </div>
- </form>
- </div>
- </div>
- <!-- 关联知识点模态框 -->
- <div id="kpRelationModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
- <div class="bg-white rounded-2xl shadow-2xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
- <div class="p-6 border-b border-gray-200">
- <h2 id="kpRelationModalTitle" class="text-xl font-bold text-gray-800">关联知识点</h2>
- </div>
- <div class="p-6">
- <div class="mb-4">
- <label class="block text-sm font-semibold text-gray-700 mb-2">选择知识点</label>
- <select id="kpCodeSelect" class="w-full px-4 py-2 border border-gray-300 rounded-lg">
- <option value="">请选择知识点</option>
- {% for kp in kp_options %}
- <option value="{{ kp.kp_code }}">{{ kp.kp_code }} - {{ kp.name }}</option>
- {% endfor %}
- </select>
- </div>
- <button onclick="addKpRelation()" class="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 mb-4">
- 添加关联
- </button>
- <div id="kpRelationList" class="space-y-2">
- <!-- 关联列表将通过JavaScript动态加载 -->
- </div>
- </div>
- <div class="p-6 border-t border-gray-200">
- <button onclick="closeKpRelationModal()" class="w-full px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">关闭</button>
- </div>
- </div>
- </div>
- <script>
- // 当前选中的系列ID和教材ID
- let currentSeriesId = null;
- let currentTextbookId = null;
- let currentCatalogChapterId = null;
- // 所有系列数据
- const allSeries = {{ series_list|tojson|safe }};
- // 初始化:加载第一个系列
- document.addEventListener('DOMContentLoaded', function() {
- const firstSeriesItem = document.querySelector('.series-item');
- if (firstSeriesItem) {
- const firstSeriesId = parseInt(firstSeriesItem.getAttribute('data-series-id'));
- switchSeries(firstSeriesId);
- }
- });
- // 筛选系列列表
- function filterSeriesList() {
- const searchInput = document.getElementById('seriesSearchInput');
- const searchTerm = (searchInput.value || '').toLowerCase().trim();
- const seriesItems = document.querySelectorAll('.series-item');
-
- let visibleCount = 0;
- seriesItems.forEach(item => {
- const seriesName = item.getAttribute('data-series-name').toLowerCase();
- const matchesSearch = !searchTerm || seriesName.includes(searchTerm);
-
- if (matchesSearch) {
- item.style.display = '';
- visibleCount++;
- } else {
- item.style.display = 'none';
- }
- });
- }
- // 切换教材系列
- async function switchSeries(seriesId) {
- // 确保seriesId是数字类型
- seriesId = parseInt(seriesId);
- if (!seriesId || isNaN(seriesId)) {
- console.error('Invalid seriesId:', seriesId);
- return;
- }
-
- currentSeriesId = seriesId;
-
- // 更新左侧菜单选中状态
- document.querySelectorAll('.series-link').forEach(link => {
- link.classList.remove('bg-gradient-to-r', 'from-slate-50', 'to-blue-50', 'border-slate-500');
- });
-
- const selectedLink = document.querySelector(`.series-item[data-series-id="${seriesId}"] .series-link`);
- if (selectedLink) {
- selectedLink.classList.add('bg-gradient-to-r', 'from-slate-50', 'to-blue-50', 'border-slate-500');
- }
-
- // 获取容器
- const container = document.getElementById('textbookListContainer');
- if (!container) {
- console.error('textbookListContainer not found');
- return;
- }
-
- // 显示教材列表容器,隐藏目录树
- container.classList.remove('hidden');
- document.getElementById('catalogTreeContainer').classList.add('hidden');
-
- // 显示加载状态
- container.innerHTML = '<div class="col-span-full text-center py-12"><i class="ri-loader-4-line animate-spin text-3xl text-blue-500"></i><p class="mt-3 text-gray-500">加载中...</p></div>';
-
- // 加载该系列下的教材
- try {
- const response = await fetch(`/api/textbook/list/${seriesId}`);
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const result = await response.json();
-
- if (result.success) {
- // 确保data是数组
- const textbooks = Array.isArray(result.data) ? result.data : [];
-
- if (textbooks.length === 0) {
- container.innerHTML = '<div class="col-span-full text-center py-12"><i class="ri-book-open-line text-4xl text-gray-300 mb-3"></i><p class="text-gray-500">暂无教材</p><p class="text-xs text-gray-400 mt-2">该系列下还没有添加教材</p></div>';
- } else {
- renderTextbookList(textbooks);
- }
- } else {
- console.error('API error:', result.error);
- container.innerHTML = '<div class="col-span-full text-center py-12"><i class="ri-error-warning-line text-4xl text-red-300 mb-3"></i><p class="text-gray-500">加载失败:' + (result.error || '未知错误') + '</p><button onclick="switchSeries(' + seriesId + ')" class="mt-4 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">重试</button></div>';
- }
- } catch (error) {
- console.error('Error loading textbooks:', error);
- container.innerHTML = '<div class="col-span-full text-center py-12"><i class="ri-error-warning-line text-4xl text-red-300 mb-3"></i><p class="text-gray-500">加载教材列表失败</p><p class="text-xs text-gray-400 mt-2">' + error.message + '</p><button onclick="switchSeries(' + seriesId + ')" class="mt-4 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">重试</button></div>';
- }
- }
- // 直接在列表中切换系列激活状态
- async function toggleSeriesActiveDirect(seriesId, labelElement) {
- const checkbox = labelElement.querySelector('input[type="checkbox"]');
- if (!checkbox) return;
-
- // 立即切换开关状态(乐观更新)
- const newState = !checkbox.checked;
- checkbox.checked = newState;
-
- // 禁用开关,防止重复点击
- checkbox.disabled = true;
-
- try {
- const response = await fetch(`/api/textbook/series/toggle_active/${seriesId}`, {
- method: 'POST',
- headers: {'Content-Type': 'application/json'}
- });
-
- const result = await response.json();
-
- if (result.success) {
- // 更新系列项的data-active属性
- const seriesItem = document.querySelector(`.series-item[data-series-id="${seriesId}"]`);
- if (seriesItem) {
- seriesItem.setAttribute('data-series-active', result.is_active);
- }
-
- // 更新allSeries数据
- const series = allSeries.find(s => s.id === seriesId);
- if (series) {
- series.is_active = result.is_active;
- }
-
- // 确保checkbox状态与服务器一致
- checkbox.checked = result.is_active === 1;
-
- // 如果当前选中的系列,刷新教材列表
- if (currentSeriesId === seriesId) {
- switchSeries(seriesId);
- }
- } else {
- // 恢复开关状态
- checkbox.checked = !newState;
- if (window.customAlert) {
- window.customAlert('操作失败: ' + result.error);
- } else {
- alert('操作失败: ' + result.error);
- }
- }
- } catch (error) {
- console.error('Error:', error);
- // 恢复开关状态
- checkbox.checked = !newState;
- if (window.customAlert) {
- window.customAlert('操作失败: ' + error.message);
- } else {
- alert('操作失败: ' + error.message);
- }
- } finally {
- checkbox.disabled = false;
- }
- }
- // 渲染教材列表
- function renderTextbookList(textbooks) {
- const container = document.getElementById('textbookListContainer');
- container.innerHTML = '';
-
- textbooks.forEach(textbook => {
- const card = document.createElement('div');
- card.className = 'group relative apple-card overflow-hidden cursor-pointer hover:shadow-2xl transition-all duration-300 hover:-translate-y-1';
- card.onclick = () => loadTextbookCatalog(textbook.id, textbook.official_title);
-
- // 生成封面占位图的渐变背景色(根据教材ID生成不同颜色)
- const colors = [
- 'from-blue-500 to-indigo-600',
- 'from-green-500 to-emerald-600',
- 'from-purple-500 to-violet-600',
- 'from-pink-500 to-rose-600',
- 'from-orange-500 to-amber-600',
- 'from-cyan-500 to-blue-600'
- ];
- const colorIndex = (textbook.id || 0) % colors.length;
- const gradientColor = colors[colorIndex];
-
- card.innerHTML = `
- <!-- 封面占位图 -->
- <div class="relative w-full aspect-[3/4] bg-gradient-to-br ${gradientColor} overflow-hidden">
- <div class="absolute inset-0 flex items-center justify-center">
- <div class="text-center text-white/90">
- <i class="ri-book-open-line text-6xl mb-2 opacity-80"></i>
- <div class="text-xs font-semibold opacity-70">教材封面</div>
- </div>
- </div>
- <!-- 封面装饰线条 -->
- <div class="absolute top-0 left-0 right-0 h-1 bg-white/20"></div>
- <div class="absolute bottom-0 left-0 right-0 h-1 bg-black/10"></div>
- <!-- 操作按钮(悬停显示) -->
- <div class="absolute top-2 right-2 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity" onclick="event.stopPropagation()">
- <button onclick="showEditTextbookModal(${textbook.id})" class="w-8 h-8 rounded-full bg-white/90 backdrop-blur-sm text-blue-600 hover:bg-white hover:scale-110 transition-all shadow-lg flex items-center justify-center">
- <i class="ri-edit-line text-sm"></i>
- </button>
- <button onclick="showDeleteTextbookConfirm(${textbook.id}, '${textbook.official_title}')" class="w-8 h-8 rounded-full bg-white/90 backdrop-blur-sm text-red-600 hover:bg-white hover:scale-110 transition-all shadow-lg flex items-center justify-center">
- <i class="ri-delete-bin-line text-sm"></i>
- </button>
- </div>
- </div>
-
- <!-- 教材信息 -->
- <div class="p-4 bg-white">
- <h3 class="text-base font-bold text-gray-800 mb-2 line-clamp-2 leading-tight">${textbook.official_title || '未命名教材'}</h3>
- <div class="flex items-center gap-2 text-xs text-gray-500">
- ${textbook.grade ? `<span class="px-2 py-0.5 bg-gray-100 rounded">${textbook.grade}年级</span>` : ''}
- ${textbook.semester ? `<span class="px-2 py-0.5 bg-gray-100 rounded">${textbook.semester === 1 ? '上学期' : '下学期'}</span>` : ''}
- </div>
- </div>
- `;
- container.appendChild(card);
- });
-
- // 添加"添加教材"卡片
- const addCard = document.createElement('div');
- addCard.className = 'apple-card overflow-hidden cursor-pointer hover:shadow-xl transition-all border-2 border-dashed border-gray-300 hover:border-blue-400';
- addCard.onclick = () => showAddTextbookModal();
- addCard.innerHTML = `
- <div class="w-full aspect-[3/4] flex flex-col items-center justify-center text-gray-400 hover:text-blue-500 transition-colors">
- <div class="mb-3">
- <i class="ri-add-circle-line text-5xl"></i>
- </div>
- <div class="text-sm font-medium">添加教材</div>
- </div>
- `;
- container.appendChild(addCard);
- }
- // 加载教材目录树
- async function loadTextbookCatalog(textbookId, textbookTitle) {
- currentTextbookId = textbookId;
- const titleElement = document.getElementById('currentTextbookTitle');
- const catalogContainer = document.getElementById('catalogTreeContainer');
- const textbookContainer = document.getElementById('textbookListContainer');
-
- if (titleElement) {
- titleElement.textContent = textbookTitle;
- }
-
- // 显示目录树,隐藏教材列表
- if (catalogContainer) {
- catalogContainer.classList.remove('hidden');
- }
- if (textbookContainer) {
- textbookContainer.classList.add('hidden');
- }
-
- try {
- const response = await fetch(`/api/textbook/catalog/tree/${textbookId}`);
- const result = await response.json();
-
- if (result.success) {
- renderCatalogTree(result.data);
- } else {
- console.error('Failed to load catalog tree:', result.error);
- if (window.customAlert) {
- window.customAlert('加载目录树失败: ' + (result.error || '未知错误'));
- } else {
- alert('加载目录树失败: ' + (result.error || '未知错误'));
- }
- }
- } catch (error) {
- console.error('Error loading catalog tree:', error);
- if (window.customAlert) {
- window.customAlert('加载目录树失败: ' + error.message);
- } else {
- alert('加载目录树失败: ' + error.message);
- }
- }
- }
- // 渲染目录树
- function renderCatalogTree(nodes) {
- const container = document.getElementById('catalogTree');
- container.innerHTML = '';
-
- // 这里需要使用Jinja2宏来渲染,但由于是动态加载,我们需要用JavaScript递归渲染
- // 为了简化,我们先用简单的HTML结构
- nodes.forEach(node => {
- container.appendChild(createCatalogNodeElement(node, 0));
- });
- }
- // 创建目录节点元素(简化版,实际应该使用服务端渲染)
- function createCatalogNodeElement(node, level) {
- const div = document.createElement('div');
- div.className = `catalog-node relative mb-3`;
- div.setAttribute('data-node-id', node.id);
- div.setAttribute('data-level', level);
-
- const hasChildren = node.children && node.children.length > 0;
- const colorClass = node.node_type === 'chapter' ? 'from-blue-500 to-indigo-600' :
- node.node_type === 'section' ? 'from-green-500 to-emerald-600' :
- 'from-orange-500 to-amber-600';
-
- div.innerHTML = `
- <div class="flex items-start gap-4 group">
- <div class="flex items-center gap-2 pt-4 flex-shrink-0">
- ${hasChildren ? `
- <button onclick="toggleCatalogNode('${node.id}'); event.stopPropagation();"
- class="w-8 h-8 rounded-full bg-white border-2 border-gray-300 hover:border-blue-500 flex items-center justify-center text-gray-600 hover:text-blue-600 transition-all expand-btn z-10 shadow-sm hover:shadow-md"
- data-expanded="${level === 0 ? 'true' : 'false'}">
- <i class="ri-${level === 0 ? 'subtract' : 'add'}-line text-sm"></i>
- </button>
- ` : `
- <div class="w-8 h-8 flex items-center justify-center">
- <div class="w-2 h-2 rounded-full bg-gray-400"></div>
- </div>
- `}
- </div>
- <div class="flex-1 apple-card p-5 hover:shadow-xl transition-all duration-300 group-hover:border-blue-300 border-2 border-transparent rounded-xl ${hasChildren ? 'cursor-pointer' : ''}"
- ${hasChildren ? `onclick="handleCatalogCardClick(event, '${node.id}')"` : ''}>
- <div class="flex items-start justify-between">
- <div class="flex-1 min-w-0">
- <div class="flex items-center gap-3 mb-3 flex-wrap">
- <span class="px-3 py-1.5 rounded-lg text-xs font-bold font-mono bg-gradient-to-r ${colorClass} text-white shadow-md">
- ${node.display_no || node.node_type}
- </span>
- <h3 class="text-lg font-bold text-gray-800 group-hover:text-blue-600 transition-colors">
- ${node.title}
- </h3>
- </div>
- <div class="flex items-center gap-4 text-sm text-gray-500 mt-2 flex-wrap">
- <span class="flex items-center gap-1.5 px-2 py-1 bg-gray-50 rounded-md">
- <i class="ri-file-list-line text-blue-500"></i>
- <span>${node.node_type}</span>
- </span>
- ${hasChildren ? `
- <span class="flex items-center gap-1.5 px-2 py-1 bg-blue-50 rounded-md text-blue-600">
- <i class="ri-node-tree"></i>
- <span class="font-semibold">${node.children.length} 个子节点</span>
- </span>
- ` : ''}
- </div>
- </div>
- <div class="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity ml-4 flex-shrink-0" onclick="event.stopPropagation()">
- ${node.node_type === 'section' ? `
- <button onclick="showAddKpRelationModal('${node.id}', '${node.title}')"
- class="px-3 py-1.5 text-xs font-semibold text-white bg-gradient-to-r from-purple-500 to-indigo-600 hover:from-purple-600 hover:to-indigo-700 rounded-lg transition-all shadow-sm hover:shadow-md flex items-center gap-1.5">
- <i class="ri-link"></i>
- <span>关联知识点</span>
- </button>
- ` : ''}
- <button onclick="showAddChildCatalogModal('${node.id}', '${node.title}', '${node.node_type}')"
- class="px-3 py-1.5 text-xs font-semibold text-white bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 rounded-lg transition-all shadow-sm hover:shadow-md flex items-center gap-1.5">
- <i class="ri-add-line"></i>
- <span>添加子节点</span>
- </button>
- <button onclick="showEditCatalogModal(${node.id})"
- class="w-9 h-9 rounded-lg text-blue-600 bg-blue-50 hover:bg-blue-100 flex items-center justify-center transition-colors shadow-sm hover:shadow-md">
- <i class="ri-edit-line"></i>
- </button>
- <button onclick="showDeleteCatalogConfirm(${node.id}, '${node.title}')"
- class="w-9 h-9 rounded-lg text-red-600 bg-red-50 hover:bg-red-100 flex items-center justify-center transition-colors shadow-sm hover:shadow-md">
- <i class="ri-delete-bin-line"></i>
- </button>
- </div>
- </div>
- </div>
- </div>
- ${hasChildren ? `
- <div class="children-container mt-3 ml-12 ${level >= 1 ? 'hidden' : ''}" id="children-${node.id}" data-level="${level}">
- ${node.children.map(child => createCatalogNodeElement(child, level + 1).outerHTML).join('')}
- </div>
- ` : ''}
- ${node.node_type === 'section' ? `
- <div class="knowledge-points-container mt-3 ml-12" id="kp-${node.id}">
- <div class="flex items-center gap-2 mb-2">
- <i class="ri-loader-4-line animate-spin text-gray-400 text-sm"></i>
- <span class="text-xs text-gray-500">加载知识点...</span>
- </div>
- </div>
- ` : ''}
- `;
-
- // 如果是section节点,异步加载关联的知识点
- if (node.node_type === 'section') {
- setTimeout(() => loadKnowledgePointsForSection(node.id), 100);
- }
-
- return div;
- }
- // 切换目录节点展开/折叠
- function toggleCatalogNode(nodeId) {
- const childrenContainer = document.getElementById(`children-${nodeId}`);
- if (!childrenContainer) return;
-
- const kpNode = childrenContainer.closest('.catalog-node');
- if (!kpNode) return;
-
- const expandBtn = kpNode.querySelector('.expand-btn');
- if (!expandBtn) return;
-
- const isExpanded = expandBtn.getAttribute('data-expanded') === 'true';
-
- if (isExpanded) {
- childrenContainer.classList.add('hidden');
- expandBtn.setAttribute('data-expanded', 'false');
- const icon = expandBtn.querySelector('i');
- if (icon) {
- icon.className = 'ri-add-line text-sm';
- }
- } else {
- childrenContainer.classList.remove('hidden');
- expandBtn.setAttribute('data-expanded', 'true');
- const icon = expandBtn.querySelector('i');
- if (icon) {
- icon.className = 'ri-subtract-line text-sm';
- }
- }
- }
- // 处理卡片点击
- function handleCatalogCardClick(event, nodeId) {
- if (event.target.closest('button') || event.target.closest('a')) {
- return;
- }
- toggleCatalogNode(nodeId);
- }
- // 加载section关联的知识点
- async function loadKnowledgePointsForSection(sectionId) {
- const container = document.getElementById(`kp-${sectionId}`);
- if (!container) return;
-
- try {
- const response = await fetch(`/api/textbook/relation/list/${sectionId}`);
- const result = await response.json();
-
- if (result.success && result.data && result.data.length > 0) {
- // 去重:按kp_code去重,保留第一个
- const uniqueRelations = [];
- const seenKpCodes = new Set();
-
- result.data.forEach(relation => {
- if (!seenKpCodes.has(relation.kp_code)) {
- seenKpCodes.add(relation.kp_code);
- uniqueRelations.push(relation);
- }
- });
-
- container.innerHTML = `
- <div class="mb-2">
- <span class="text-xs font-semibold text-purple-600 flex items-center gap-1">
- <i class="ri-link"></i>
- <span>关联知识点 (${uniqueRelations.length})</span>
- </span>
- </div>
- <div class="flex flex-wrap gap-2">
- ${uniqueRelations.map(relation => `
- <div class="group relative px-3 py-2 bg-gradient-to-r from-purple-50 to-indigo-50 border border-purple-200 rounded-lg hover:shadow-md transition-all">
- <div class="flex items-center gap-2">
- <span class="text-xs font-mono font-bold text-purple-600">${relation.kp_code || ''}</span>
- <span class="text-sm text-gray-700">${relation.kp_name || '未知知识点'}</span>
- <button onclick="removeKpRelationByCode('${relation.kp_code}', ${sectionId})"
- class="opacity-0 group-hover:opacity-100 ml-1 w-5 h-5 rounded-full bg-red-100 hover:bg-red-200 text-red-600 flex items-center justify-center transition-all"
- title="取消关联">
- <i class="ri-close-line text-xs"></i>
- </button>
- </div>
- </div>
- `).join('')}
- </div>
- `;
- } else {
- container.innerHTML = `
- <div class="text-xs text-gray-400 italic">
- <i class="ri-information-line"></i>
- <span>暂无关联知识点</span>
- </div>
- `;
- }
- } catch (error) {
- console.error('Error loading knowledge points:', error);
- container.innerHTML = `
- <div class="text-xs text-red-400">
- <i class="ri-error-warning-line"></i>
- <span>加载知识点失败</span>
- </div>
- `;
- }
- }
- // 移除知识点关联(通过relationId)
- async function removeKpRelation(relationId, sectionId) {
- if (!confirm('确定要取消这个知识点关联吗?')) {
- return;
- }
-
- try {
- const response = await fetch(`/api/textbook/relation/delete/${relationId}`, {
- method: 'POST',
- headers: {'Content-Type': 'application/json'}
- });
-
- const result = await response.json();
-
- if (result.success) {
- // 重新加载知识点列表
- loadKnowledgePointsForSection(sectionId);
-
- if (window.customAlert) {
- window.customAlert('已取消关联');
- } else {
- alert('已取消关联');
- }
- } else {
- if (window.customAlert) {
- window.customAlert('操作失败: ' + result.error);
- } else {
- alert('操作失败: ' + result.error);
- }
- }
- } catch (error) {
- console.error('Error:', error);
- if (window.customAlert) {
- window.customAlert('操作失败: ' + error.message);
- } else {
- alert('操作失败: ' + error.message);
- }
- }
- }
- // 移除知识点关联(通过kp_code,删除所有重复的关联)
- async function removeKpRelationByCode(kpCode, sectionId) {
- if (!confirm('确定要取消这个知识点关联吗?')) {
- return;
- }
-
- try {
- // 先获取所有该知识点的关联
- const listResponse = await fetch(`/api/textbook/relation/list/${sectionId}`);
- const listResult = await listResponse.json();
-
- if (!listResult.success || !listResult.data) {
- throw new Error('获取关联列表失败');
- }
-
- // 找到所有匹配的关联ID
- const relationIds = listResult.data
- .filter(rel => rel.kp_code === kpCode)
- .map(rel => rel.id);
-
- if (relationIds.length === 0) {
- if (window.customAlert) {
- window.customAlert('未找到关联');
- } else {
- alert('未找到关联');
- }
- return;
- }
-
- // 删除所有重复的关联
- const deletePromises = relationIds.map(id =>
- fetch(`/api/textbook/relation/delete/${id}`, {
- method: 'POST',
- headers: {'Content-Type': 'application/json'}
- })
- );
-
- await Promise.all(deletePromises);
-
- // 重新加载知识点列表
- loadKnowledgePointsForSection(sectionId);
-
- // 如果模态框打开,也刷新模态框中的列表
- if (currentKpRelationSectionId === sectionId) {
- await loadKpRelations(sectionId);
- }
-
- if (window.customAlert) {
- window.customAlert('已取消关联');
- } else {
- alert('已取消关联');
- }
- } catch (error) {
- console.error('Error:', error);
- if (window.customAlert) {
- window.customAlert('操作失败: ' + error.message);
- } else {
- alert('操作失败: ' + error.message);
- }
- }
- }
- // ==================== 教材系列 CRUD ====================
- function showAddSeriesModal() {
- document.getElementById('seriesModalTitle').textContent = '添加教材系列';
- document.getElementById('seriesForm').reset();
- document.getElementById('seriesId').value = '';
- document.getElementById('seriesIsActive').checked = true; // 默认激活
- document.getElementById('seriesModal').classList.remove('hidden');
- }
- async function showEditSeriesModal(seriesId) {
- try {
- const response = await fetch(`/api/textbook/series/get/${seriesId}`);
- const result = await response.json();
-
- if (result.success) {
- const series = result.data;
- document.getElementById('seriesModalTitle').textContent = '编辑教材系列';
- document.getElementById('seriesId').value = series.id;
- document.getElementById('seriesName').value = series.name || '';
- document.getElementById('seriesSlug').value = series.slug || '';
- document.getElementById('seriesIsActive').checked = series.is_active === 1;
- document.getElementById('seriesModal').classList.remove('hidden');
- }
- } catch (error) {
- console.error('Error:', error);
- if (window.customAlert) {
- window.customAlert('获取系列信息失败: ' + error.message);
- } else {
- alert('获取系列信息失败');
- }
- }
- }
- function closeSeriesModal() {
- document.getElementById('seriesModal').classList.add('hidden');
- }
- document.getElementById('seriesForm').addEventListener('submit', async function(e) {
- e.preventDefault();
-
- const seriesId = document.getElementById('seriesId').value;
- const formData = {
- name: document.getElementById('seriesName').value.trim(),
- slug: document.getElementById('seriesSlug').value.trim() || null,
- is_active: document.getElementById('seriesIsActive').checked
- };
-
- try {
- let response;
- if (seriesId) {
- response = await fetch(`/api/textbook/series/update/${seriesId}`, {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify(formData)
- });
- } else {
- response = await fetch('/api/textbook/series/create', {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify(formData)
- });
- }
-
- const result = await response.json();
-
- if (result.success) {
- alert(result.message || '操作成功');
- window.location.reload();
- } else {
- alert('操作失败: ' + result.error);
- }
- } catch (error) {
- console.error('Error:', error);
- alert('操作失败: ' + error.message);
- }
- });
- // ==================== 教材 CRUD ====================
- function showAddTextbookModal() {
- document.getElementById('textbookModalTitle').textContent = '添加教材';
- document.getElementById('textbookForm').reset();
- document.getElementById('textbookId').value = '';
- document.getElementById('textbookModal').classList.remove('hidden');
- }
- async function showEditTextbookModal(textbookId) {
- try {
- const response = await fetch(`/api/textbook/get/${textbookId}`);
- const result = await response.json();
-
- if (result.success) {
- const textbook = result.data;
- document.getElementById('textbookModalTitle').textContent = '编辑教材';
- document.getElementById('textbookId').value = textbook.id;
- document.getElementById('textbookTitle').value = textbook.official_title || '';
- document.getElementById('textbookStage').value = textbook.stage || '';
- document.getElementById('textbookGrade').value = textbook.grade || '';
- document.getElementById('textbookSemester').value = textbook.semester || '';
- document.getElementById('textbookModal').classList.remove('hidden');
- }
- } catch (error) {
- console.error('Error:', error);
- alert('获取教材信息失败');
- }
- }
- function closeTextbookModal() {
- document.getElementById('textbookModal').classList.add('hidden');
- }
- document.getElementById('textbookForm').addEventListener('submit', async function(e) {
- e.preventDefault();
-
- const textbookId = document.getElementById('textbookId').value;
- const formData = {
- series_id: currentSeriesId,
- official_title: document.getElementById('textbookTitle').value.trim(),
- stage: document.getElementById('textbookStage').value || null,
- grade: document.getElementById('textbookGrade').value.trim() || null,
- semester: document.getElementById('textbookSemester').value ? parseInt(document.getElementById('textbookSemester').value) : null
- };
-
- try {
- let response;
- if (textbookId) {
- response = await fetch(`/api/textbook/update/${textbookId}`, {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify(formData)
- });
- } else {
- response = await fetch('/api/textbook/create', {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify(formData)
- });
- }
-
- const result = await response.json();
-
- if (result.success) {
- if (window.customAlert) {
- window.customAlert(result.message || '操作成功', () => {
- switchSeries(currentSeriesId);
- closeTextbookModal();
- });
- } else {
- alert(result.message || '操作成功');
- switchSeries(currentSeriesId);
- closeTextbookModal();
- }
- } else {
- if (window.customAlert) {
- window.customAlert('操作失败: ' + result.error);
- } else {
- alert('操作失败: ' + result.error);
- }
- }
- } catch (error) {
- console.error('Error:', error);
- alert('操作失败: ' + error.message);
- }
- });
- async function showDeleteTextbookConfirm(textbookId, textbookTitle) {
- if (confirm(`确定要删除教材 "${textbookTitle}" 吗?`)) {
- try {
- const response = await fetch(`/api/textbook/delete/${textbookId}`, {
- method: 'POST',
- headers: {'Content-Type': 'application/json'}
- });
-
- const result = await response.json();
-
- if (result.success) {
- if (window.customAlert) {
- window.customAlert(result.message || '删除成功', () => {
- switchSeries(currentSeriesId);
- });
- } else {
- alert(result.message || '删除成功');
- switchSeries(currentSeriesId);
- }
- } else {
- if (window.customAlert) {
- window.customAlert('删除失败: ' + result.error);
- } else {
- alert('删除失败: ' + result.error);
- }
- }
- } catch (error) {
- console.error('Error:', error);
- alert('删除失败: ' + error.message);
- }
- }
- }
- // ==================== 目录节点 CRUD ====================
- function showAddCatalogModal(parentId = null, parentNodeType = null) {
- document.getElementById('catalogModalTitle').textContent = parentId ? '添加子节点' : '添加目录节点';
- document.getElementById('catalogForm').reset();
- document.getElementById('catalogId').value = '';
- document.getElementById('catalogTextbookId').value = currentTextbookId;
- document.getElementById('catalogParentId').value = parentId || '';
-
- // 根据父节点类型限制子节点类型
- const nodeTypeSelect = document.getElementById('catalogNodeType');
- const allOptions = nodeTypeSelect.querySelectorAll('option');
-
- // 先显示所有选项
- allOptions.forEach(opt => opt.style.display = '');
-
- if (parentId && parentNodeType) {
- // 有父节点,根据父节点类型限制
- if (parentNodeType === 'chapter') {
- // 章节下只能创建 section
- allOptions.forEach(opt => {
- if (opt.value !== 'section') {
- opt.style.display = 'none';
- }
- });
- nodeTypeSelect.value = 'section';
- } else if (parentNodeType === 'section') {
- // section 下只能创建 subsection
- allOptions.forEach(opt => {
- if (opt.value !== 'subsection') {
- opt.style.display = 'none';
- }
- });
- nodeTypeSelect.value = 'subsection';
- } else {
- // subsection 下不能再创建子节点(但这里不应该被调用)
- allOptions.forEach(opt => {
- if (opt.value !== 'subsection') {
- opt.style.display = 'none';
- }
- });
- }
- } else {
- // 没有父节点,只能创建顶级节点(chapter)
- allOptions.forEach(opt => {
- if (opt.value !== 'chapter') {
- opt.style.display = 'none';
- }
- });
- nodeTypeSelect.value = 'chapter';
- }
-
- document.getElementById('catalogModal').classList.remove('hidden');
- }
- async function showAddChildCatalogModal(parentId, parentTitle, parentNodeType = null) {
- // 如果未传递父节点类型,则通过API获取
- if (!parentNodeType) {
- try {
- const response = await fetch(`/api/textbook/catalog/get/${parentId}`);
- const result = await response.json();
-
- if (result.success) {
- parentNodeType = result.data.node_type;
- }
- } catch (error) {
- console.error('获取父节点信息失败:', error);
- }
- }
-
- showAddCatalogModal(parentId, parentNodeType);
- }
- async function showEditCatalogModal(nodeId) {
- try {
- const response = await fetch(`/api/textbook/catalog/get/${nodeId}`);
- const result = await response.json();
-
- if (result.success) {
- const node = result.data;
- document.getElementById('catalogModalTitle').textContent = '编辑目录节点';
- document.getElementById('catalogId').value = node.id;
- document.getElementById('catalogTextbookId').value = node.textbook_id;
- document.getElementById('catalogParentId').value = node.parent_id || '';
- document.getElementById('catalogTitle').value = node.title || '';
- document.getElementById('catalogDisplayNo').value = node.display_no || '';
-
- // 根据父节点类型限制节点类型选择
- const nodeTypeSelect = document.getElementById('catalogNodeType');
- const allOptions = nodeTypeSelect.querySelectorAll('option');
-
- // 先显示所有选项
- allOptions.forEach(opt => opt.style.display = '');
-
- if (node.parent_id) {
- // 有父节点,需要获取父节点类型
- try {
- const parentResponse = await fetch(`/api/textbook/catalog/get/${node.parent_id}`);
- const parentResult = await parentResponse.json();
-
- if (parentResult.success) {
- const parentNodeType = parentResult.data.node_type;
- if (parentNodeType === 'chapter') {
- // 章节下只能创建 section
- allOptions.forEach(opt => {
- if (opt.value !== 'section') {
- opt.style.display = 'none';
- }
- });
- } else if (parentNodeType === 'section') {
- // section 下只能创建 subsection
- allOptions.forEach(opt => {
- if (opt.value !== 'subsection') {
- opt.style.display = 'none';
- }
- });
- }
- }
- } catch (error) {
- console.error('获取父节点信息失败:', error);
- }
- } else {
- // 没有父节点,只能创建顶级节点(chapter)
- allOptions.forEach(opt => {
- if (opt.value !== 'chapter') {
- opt.style.display = 'none';
- }
- });
- }
-
- document.getElementById('catalogNodeType').value = node.node_type || 'chapter';
- document.getElementById('catalogModal').classList.remove('hidden');
- }
- } catch (error) {
- console.error('Error:', error);
- alert('获取节点信息失败');
- }
- }
- function closeCatalogModal() {
- document.getElementById('catalogModal').classList.add('hidden');
- }
- document.getElementById('catalogForm').addEventListener('submit', async function(e) {
- e.preventDefault();
-
- const catalogId = document.getElementById('catalogId').value;
- const formData = {
- textbook_id: parseInt(document.getElementById('catalogTextbookId').value),
- parent_id: document.getElementById('catalogParentId').value ? parseInt(document.getElementById('catalogParentId').value) : null,
- node_type: document.getElementById('catalogNodeType').value,
- title: document.getElementById('catalogTitle').value.trim(),
- display_no: document.getElementById('catalogDisplayNo').value.trim() || null
- };
-
- try {
- let response;
- if (catalogId) {
- response = await fetch(`/api/textbook/catalog/update/${catalogId}`, {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify(formData)
- });
- } else {
- response = await fetch('/api/textbook/catalog/create', {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify(formData)
- });
- }
-
- const result = await response.json();
-
- if (result.success) {
- if (window.customAlert) {
- window.customAlert(result.message || '操作成功', () => {
- loadTextbookCatalog(currentTextbookId, document.getElementById('currentTextbookTitle').textContent);
- closeCatalogModal();
- });
- } else {
- alert(result.message || '操作成功');
- loadTextbookCatalog(currentTextbookId, document.getElementById('currentTextbookTitle').textContent);
- closeCatalogModal();
- }
- } else {
- if (window.customAlert) {
- window.customAlert('操作失败: ' + result.error);
- } else {
- alert('操作失败: ' + result.error);
- }
- }
- } catch (error) {
- console.error('Error:', error);
- alert('操作失败: ' + error.message);
- }
- });
- async function showDeleteCatalogConfirm(nodeId, nodeTitle) {
- if (confirm(`确定要删除节点 "${nodeTitle}" 吗?`)) {
- try {
- const response = await fetch(`/api/textbook/catalog/delete/${nodeId}`, {
- method: 'POST',
- headers: {'Content-Type': 'application/json'}
- });
-
- const result = await response.json();
-
- if (result.success) {
- if (window.customAlert) {
- window.customAlert(result.message || '删除成功', () => {
- loadTextbookCatalog(currentTextbookId, document.getElementById('currentTextbookTitle').textContent);
- });
- } else {
- alert(result.message || '删除成功');
- loadTextbookCatalog(currentTextbookId, document.getElementById('currentTextbookTitle').textContent);
- }
- } else {
- if (window.customAlert) {
- window.customAlert('删除失败: ' + result.error);
- } else {
- alert('删除失败: ' + result.error);
- }
- }
- } catch (error) {
- console.error('Error:', error);
- alert('删除失败: ' + error.message);
- }
- }
- }
- // ==================== 知识点关联 CRUD ====================
- let currentKpRelationSectionId = null;
- async function showAddKpRelationModal(chapterId, chapterTitle) {
- currentKpRelationSectionId = chapterId;
- currentCatalogChapterId = chapterId;
- document.getElementById('kpRelationModalTitle').textContent = `关联知识点 - ${chapterTitle}`;
- document.getElementById('kpRelationModal').classList.remove('hidden');
-
- // 加载已有的关联
- await loadKpRelations(chapterId);
- }
- function closeKpRelationModal() {
- document.getElementById('kpRelationModal').classList.add('hidden');
- currentCatalogChapterId = null;
- }
- async function loadKpRelations(chapterId) {
- try {
- const response = await fetch(`/api/textbook/relation/list/${chapterId}`);
- const result = await response.json();
-
- if (result.success) {
- const container = document.getElementById('kpRelationList');
- container.innerHTML = '';
-
- if (result.data.length === 0) {
- container.innerHTML = '<p class="text-gray-500 text-center py-4">暂无关联的知识点</p>';
- } else {
- // 去重:按kp_code去重,保留第一个
- const uniqueRelations = [];
- const seenKpCodes = new Set();
-
- result.data.forEach(relation => {
- if (!seenKpCodes.has(relation.kp_code)) {
- seenKpCodes.add(relation.kp_code);
- uniqueRelations.push(relation);
- }
- });
-
- uniqueRelations.forEach(relation => {
- const div = document.createElement('div');
- div.className = 'flex items-center justify-between p-3 bg-gray-50 rounded-lg';
- div.innerHTML = `
- <div>
- <span class="font-semibold">${relation.kp_code}</span>
- ${relation.kp_name ? `<span class="text-gray-500 ml-2">${relation.kp_name}</span>` : ''}
- </div>
- <button onclick="deleteKpRelation(${relation.id})" class="text-red-600 hover:text-red-800">
- <i class="ri-delete-bin-line"></i>
- </button>
- `;
- container.appendChild(div);
- });
- }
- }
- } catch (error) {
- console.error('Error:', error);
- }
- }
- async function addKpRelation() {
- const kpCode = document.getElementById('kpCodeSelect').value;
- if (!kpCode) {
- if (window.customAlert) {
- window.customAlert('请选择知识点');
- } else {
- alert('请选择知识点');
- }
- return;
- }
-
- // 检查是否已存在该关联
- try {
- const checkResponse = await fetch(`/api/textbook/relation/list/${currentCatalogChapterId}`);
- const checkResult = await checkResponse.json();
-
- if (checkResult.success && checkResult.data) {
- const exists = checkResult.data.some(rel => rel.kp_code === kpCode);
- if (exists) {
- if (window.customAlert) {
- window.customAlert('该知识点已关联,不能重复添加');
- } else {
- alert('该知识点已关联,不能重复添加');
- }
- return;
- }
- }
- } catch (error) {
- console.error('Error checking existing relations:', error);
- }
-
- try {
- const response = await fetch('/api/textbook/relation/create', {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({
- catalog_chapter_id: currentCatalogChapterId,
- kp_code: kpCode
- })
- });
-
- const result = await response.json();
-
- if (result.success) {
- document.getElementById('kpCodeSelect').value = '';
- await loadKpRelations(currentCatalogChapterId);
- // 刷新section下显示的知识点列表
- if (currentKpRelationSectionId) {
- loadKnowledgePointsForSection(currentKpRelationSectionId);
- }
- } else {
- if (window.customAlert) {
- window.customAlert('添加失败: ' + result.error);
- } else {
- alert('添加失败: ' + result.error);
- }
- }
- } catch (error) {
- console.error('Error:', error);
- if (window.customAlert) {
- window.customAlert('添加失败: ' + error.message);
- } else {
- alert('添加失败: ' + error.message);
- }
- }
- }
- async function deleteKpRelation(relationId) {
- if (!confirm('确定要删除这个关联吗?')) {
- return;
- }
-
- try {
- const response = await fetch(`/api/textbook/relation/delete/${relationId}`, {
- method: 'POST',
- headers: {'Content-Type': 'application/json'}
- });
-
- const result = await response.json();
-
- if (result.success) {
- await loadKpRelations(currentCatalogChapterId);
- // 刷新section下显示的知识点列表
- if (currentKpRelationSectionId) {
- loadKnowledgePointsForSection(currentKpRelationSectionId);
- }
- } else {
- alert('删除失败: ' + result.error);
- }
- } catch (error) {
- console.error('Error:', error);
- alert('删除失败: ' + error.message);
- }
- }
- // 点击模态框外部关闭
- document.getElementById('seriesModal').addEventListener('click', function(e) {
- if (e.target === this) closeSeriesModal();
- });
- document.getElementById('textbookModal').addEventListener('click', function(e) {
- if (e.target === this) closeTextbookModal();
- });
- document.getElementById('catalogModal').addEventListener('click', function(e) {
- if (e.target === this) closeCatalogModal();
- });
- document.getElementById('kpRelationModal').addEventListener('click', function(e) {
- if (e.target === this) closeKpRelationModal();
- });
- </script>
- {% endblock %}
|