Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
.timetable { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; }
.timetable th, .timetable td { border: 1px solid #ddd; padding: 5px; text-align: center; font-size: 0.85rem; height: 40px; }
.timetable th { background: #f9f9f9; }
.cell-active { background-color: #bbdefb; color: #0d47a1; font-size: 0.75rem; border-radius: 2px; height: 100%; width: 100%;}
.cell-active { background-color: #bbdefb; color: #0d47a1; font-size: 0.75rem; border-radius: 2px; height: 100%; width: 100%; display: flex; flex-direction: column; justify-content: center; overflow: hidden; }
.cell-active.gray { background-color: #e0e0e0; color: #757575; opacity: 0.8; }
.cell-conflict { background-color: #ffcdd2; color: #b71c1c; }

/* Toast */
Expand Down Expand Up @@ -271,8 +272,13 @@ <h3>推荐方案 (显示 {{ schedules.length }} 个 / 共 {{ totalCount }} 个
<div id="capture-area" style="background: white; padding: 15px; margin-top: 20px; border: 1px solid #eee;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h2 style="margin:0;">第 {{ currentScheduleIdx + 1 }} 方案课表</h2>
<div>
周次: <input type="number" v-model.number="currentWeek" min="1" max="20" style="width: 60px;">
<div style="display: flex; align-items: center; gap: 10px;">
<label style="font-size: 0.9rem; cursor: pointer;">
<input type="checkbox" v-model="showAllWeeks"> 显示非本周课程
</label>
<div>
周次: <input type="number" v-model.number="currentWeek" min="1" max="20" style="width: 60px;">
</div>
</div>
</div>

Expand Down Expand Up @@ -310,7 +316,7 @@ <h2 style="margin:0;">第 {{ currentScheduleIdx + 1 }} 方案课表</h2>
<td>{{ node }}</td>
<td v-for="day in 7" :key="day">
<div v-if="getCell(currentScheduleIdx, currentWeek, day-1, node-1)"
class="cell-active"
:class="['cell-active', getCell(currentScheduleIdx, currentWeek, day-1, node-1).isCurrent ? '' : 'gray']"
@click="openAlternatives(getCell(currentScheduleIdx, currentWeek, day-1, node-1))"
style="cursor: pointer;">
<div style="font-weight: bold;">{{ getCell(currentScheduleIdx, currentWeek, day-1, node-1).name }}</div>
Expand Down
121 changes: 97 additions & 24 deletions dist/static/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ createApp({
const totalCount = ref(0);
const currentScheduleIdx = ref(0);
const currentWeek = ref(1);
const showAllWeeks = ref(false);
const toastRef = ref(null);

// Import Modal State
Expand Down Expand Up @@ -141,6 +142,27 @@ createApp({
showImportModal.value = false;
};

const checkForDuplicates = (newCandidates) => {
if (!groups.value || groups.value.length === 0) return true;
const existingNames = new Set(groups.value.map(g => {
const active = g.candidates.find(c => c.selected);
return active ? active.name : (g.candidates[0] ? g.candidates[0].name : "");
}));

const duplicates = new Set();
for (const cand of newCandidates) {
if (existingNames.has(cand.name)) {
duplicates.add(cand.name);
}
}

if (duplicates.size > 0) {
const names = Array.from(duplicates).join(", ");
return confirm(`检测到重复课程: [${names}] 已在现有课程组中。\n\n重复添加可能导致排课结果混乱。\n是否继续添加?`);
}
return true;
};

const startBatchImport = async () => {
if (!importText.value) return showToast("请粘贴内容", 'error');
isImporting.value = true;
Expand Down Expand Up @@ -218,11 +240,18 @@ createApp({
});

if (res && res.length > 0) {
// Create Group
const candidates = res.map(c => ({
const candidates = res.map(c => ({
...c,
selected: true // Auto select all for imported groups
selected: true
}));

// Check dupe for this specific import item
if (!checkForDuplicates(candidates)) {
failCount++; // User skipped
continue;
}

// Create Group
groups.value.push({
id: Date.now() + i, // unique-ish id
open: false,
Expand All @@ -248,6 +277,11 @@ createApp({
const selectedInSearch = searchResults.value.filter(c => c.checked);
if (selectedInSearch.length === 0) return showToast("未选择任何课程", 'error');

// Pre-check duplicates
if (!checkForDuplicates(selectedInSearch)) {
return;
}

// Copy all search results, map checked to selected
const candidates = searchResults.value.map(c => ({
...c,
Expand Down Expand Up @@ -309,18 +343,14 @@ createApp({
const bitPos = BigInt(day * 13 + node);
const mask = 1n << bitPos;

// 1. Try to find match in CURRENT week
for (let c of courses) {
// schedule_bitmaps are strings now
let weekMapStr = c.schedule_bitmaps ? c.schedule_bitmaps[week] : "0";
if (!weekMapStr) weekMapStr = "0";

let weekMap = 0n;
try {
weekMap = BigInt(weekMapStr);
} catch (e) {}
try { weekMap = BigInt(weekMapStr); } catch (e) {}

if ((weekMap & mask) !== 0n) {
// Found the course occupying this cell
let loc = "未知地点";
if (c.sessions) {
const currentPeriod = node + 1;
Expand All @@ -330,29 +360,71 @@ createApp({
currentPeriod <= s.end &&
s.weeks.includes(week)
);
if (sess) {
loc = sess.location;
}
if (sess) loc = sess.location;
} else {
let text = c.location_text || "";
text = text.replace(/周[一二三四五六日].+?周(\((单|双)\))?/g, '').trim();
text = text.replace(/^周[一二三四五六日]\s+/, '').trim();

if (!text || text === ',') {
loc = (c.location_text || "").split(' ').pop();
} else {
loc = text;
}
// Fallback parsing
let text = c.location_text || "";
text = text.replace(/周[一二三四五六日].+?周(\((单|双)\))?/g, '').trim();
text = text.replace(/^周[一二三四五六日]\s+/, '').trim();
if (!text || text === ',') {
loc = (c.location_text || "").split(' ').pop();
} else {
loc = text;
}
}

return {
name: c.name,
teacher: c.teacher,
location: loc,
alternatives: c.alternatives
alternatives: c.alternatives,
isCurrent: true
};
}
}

// 2. If not found, and showAllWeeks is ON, check other weeks
if (showAllWeeks.value) {
for (let c of courses) {
// Check sessions first (more accurate)
if (c.sessions) {
const currentPeriod = node + 1;
// Find any session that covers this Day/Node, regardless of week
const sess = c.sessions.find(s =>
s.day === day &&
currentPeriod >= s.start &&
currentPeriod <= s.end
);
if (sess) {
return {
name: c.name,
teacher: c.teacher,
location: sess.location,
alternatives: c.alternatives,
isCurrent: false // Gray out
};
}
}

// Fallback: scan all bitmaps (heavy but necessary if no sessions)
// Skip if sessions existed but didn't match (already handled above)
if (!c.sessions && c.schedule_bitmaps) {
for(let w=1; w<c.schedule_bitmaps.length; w++) {
let wm = 0n;
try { wm = BigInt(c.schedule_bitmaps[w]); } catch(e){}
if ((wm & mask) !== 0n) {
// Found in some week
return {
name: c.name,
teacher: c.teacher,
location: c.location_text, // Raw text fallback
alternatives: c.alternatives,
isCurrent: false
};
}
}
}
}
}
return null;
};

Expand Down Expand Up @@ -464,7 +536,8 @@ createApp({
toggleSelectAll, toggleAllDays, invertDays,
showImportModal, importText, isImporting, importStatus, importParams,
openImportModal, closeImportModal, startBatchImport,
showAltModal, currentAltCourse, openAlternatives
showAltModal, currentAltCourse, openAlternatives,
showAllWeeks
};
}
}).mount('#app');
8 changes: 6 additions & 2 deletions dist/static/js/solver.js
Original file line number Diff line number Diff line change
Expand Up @@ -463,10 +463,14 @@ class ScheduleSolver {
let totalCredits = 0;
let totalHours = 0;
const weekSet = new Set();
const countedCourses = new Set(); // Prevent double counting credits/hours for same-named courses

sched.forEach(c => {
totalCredits += (c.credit || 0);
totalHours += (c.hours || 0);
if (!countedCourses.has(c.name)) {
totalCredits += (c.credit || 0);
totalHours += (c.hours || 0);
countedCourses.add(c.name);
}
if (c.sessions) {
c.sessions.forEach(s => s.weeks.forEach(w => weekSet.add(w)));
}
Expand Down
64 changes: 64 additions & 0 deletions reproduce_issue2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@

import re

WEEKDAY_MAP = {"一": 0, "二": 1, "三": 2, "四": 3, "五": 4, "六": 5, "日": 6, "天": 6}

class ScheduleBitmapper:
REGEX_PATTERN = re.compile(r"周([一二三四五六日])\s*(\d+)-(\d+)节\s*([0-9,-]+)周")

@staticmethod
def parse_week_ranges(week_str):
weeks = set()
parts = week_str.split(',')
for part in parts:
if '-' in part:
try:
s, e = map(int, part.split('-'))
weeks.update(range(s, e + 1))
except: pass
else:
try:
weeks.add(int(part))
except: pass
return sorted(list(weeks))

@staticmethod
def generate_bitmap(location_text, max_weeks=25):
semester_schedule = [0] * (max_weeks + 1)
sessions = []

if not location_text:
return [str(x) for x in semester_schedule], sessions

segments = re.split(r'[,;]', location_text)

for seg in segments:
print(f"Processing segment: '{seg}'")
matches = list(ScheduleBitmapper.REGEX_PATTERN.finditer(seg))
if not matches:
print(" No matches found!")
continue

print(f" Found {len(matches)} matches.")

location_part = seg
for m in matches:
location_part = location_part.replace(m.group(0), "")
location_part = location_part.replace("(单)", "").replace("(双)", "").strip()

for match in matches:
day_char, start_node, end_node, week_range_str = match.groups()
print(f" Match: Day={day_char}, Start={start_node}, End={end_node}, Weeks={week_range_str}")

day_idx = WEEKDAY_MAP.get(day_char, 0)
s_node = int(start_node)
e_node = int(end_node)
active_weeks = ScheduleBitmapper.parse_week_ranges(week_range_str)

print(f" Active Weeks: {active_weeks}")

# ... (rest of logic)

test_string = "周四 9-11节 3-16周 科技馆210、216、301、302、305、308、310、311、313/315/316/318"
print(f"Testing string: {test_string}")
ScheduleBitmapper.generate_bitmap(test_string)
Binary file removed verification/planning_view.png
Binary file not shown.
Binary file removed verification/search_results.png
Binary file not shown.
Binary file removed verification/search_view.png
Binary file not shown.
89 changes: 0 additions & 89 deletions verification/server.py

This file was deleted.

Binary file removed verification/spa_failed.png
Binary file not shown.
Binary file removed verification/spa_missing_courses.png
Binary file not shown.
Loading