feat: 导出按班级/组别筛选 + 随机抽取页 + 零依赖 XLSX 导出
导出跟随分类筛选 - api.php export / export_absent 新增 cls、grp 参数,按当前筛选过滤 rows - 缺考口径与页面完全一致:筛选后名单 − 筛选后已提交(按学号匹配) - 导出文件名附带班级 / 组别(如 成绩明细_高一数学月考_高一(1)班_A组_时间戳.csv) - app.js 两个导出按钮追加 cls/grp 参数并提示导出范围 随机抽取页 draw.php - 来源:学号名单 / 某试卷已提交人员;支持班级、组别筛选与抽取人数设置 - 「排除已抽中」累计去重,抽取结果存入 localStorage,刷新后仍可下载 - 抽取动画(滚动定格),结果表格含序号 / 学号 / 姓名 / 班级 / 组别 - 下载「白名单导入模板.xlsx」「白名单列表.xlsx」:两列 姓名 / 登陆账号 XLSX 导出能力 - 新增 lib/XlsxWriter.php:零依赖生成 xlsx,有 zip 扩展走 ZipArchive, 无则回退手写 ZIP(stored);单元格按共享字符串 + 文本格式(numFmtId=49) 写入,长学号不会变科学计数法 - 新增 api.php action=draw_export,输出与「白名单列表.xlsx」模板一致的结构 其他 - index.php 顶栏新增「随机抽取」入口;支持 index.php#roster 直接打开名单管理 - tools/test-xlsxwriter.php 自测:18 项全通过(含无 ZipArchive 的手写打包路径)
This commit is contained in:
+23
-2
@@ -92,6 +92,14 @@
|
||||
el.className = 'alert' + (type === 'info' ? ' info' : '');
|
||||
}
|
||||
|
||||
// 导出提示:说明本次导出的筛选范围
|
||||
function exportTip(what, cls, grp) {
|
||||
const parts = [];
|
||||
if (cls) parts.push(cls + (cls.indexOf('班') >= 0 ? '' : '班'));
|
||||
if (grp) parts.push(grp + (grp.indexOf('组') >= 0 ? '' : '组'));
|
||||
return '已导出' + what + (parts.length ? '(' + parts.join(' / ') + ')' : '(全部班级 / 组别)');
|
||||
}
|
||||
|
||||
/* ---------------- 问卷列表 ---------------- */
|
||||
// 把数组渲染到 #projectSel(抽出便于缓存复用)
|
||||
function renderProjects() {
|
||||
@@ -661,17 +669,24 @@
|
||||
});
|
||||
});
|
||||
|
||||
// 导出跟随当前班级 / 组别筛选(统计卡片与表格显示什么,就导出什么)
|
||||
$('#btnExport').addEventListener('click', () => {
|
||||
const pid = $('#projectSel').value;
|
||||
if (!pid) return showAlert('请先选择问卷 / 考试');
|
||||
const cls = $('#clsSel').value, grp = $('#grpSel').value;
|
||||
location.href = 'api.php?action=export&projectId=' + encodeURIComponent(pid) +
|
||||
'&nameQid=' + encodeURIComponent($('#nameFieldSel').value);
|
||||
'&nameQid=' + encodeURIComponent($('#nameFieldSel').value) +
|
||||
'&cls=' + encodeURIComponent(cls) + '&grp=' + encodeURIComponent(grp);
|
||||
showAlert(exportTip('成绩明细', cls, grp), 'info');
|
||||
});
|
||||
$('#btnExportAbsent').addEventListener('click', () => {
|
||||
const pid = $('#projectSel').value;
|
||||
if (!pid) return showAlert('请先选择问卷 / 考试');
|
||||
const cls = $('#clsSel').value, grp = $('#grpSel').value;
|
||||
location.href = 'api.php?action=export_absent&projectId=' + encodeURIComponent(pid) +
|
||||
'&nameQid=' + encodeURIComponent($('#nameFieldSel').value);
|
||||
'&nameQid=' + encodeURIComponent($('#nameFieldSel').value) +
|
||||
'&cls=' + encodeURIComponent(cls) + '&grp=' + encodeURIComponent(grp);
|
||||
showAlert(exportTip('缺考名单', cls, grp), 'info');
|
||||
});
|
||||
|
||||
/* 弹窗关闭 */
|
||||
@@ -803,6 +818,12 @@
|
||||
// 退出登录:跳转到独立 logout.php(清服务端会话)
|
||||
$('#btnLogout').addEventListener('click', () => { location.href = 'logout.php'; });
|
||||
|
||||
// 3) 支持 index.php#roster 直接打开名单管理(随机抽取页的「名单管理」入口用)
|
||||
if (location.hash === '#roster') {
|
||||
openRoster();
|
||||
if (history.replaceState) history.replaceState(null, '', location.pathname + location.search);
|
||||
}
|
||||
|
||||
// 启动:主界面在 PHP 端已做会话校验(未登录会跳到 login.php),此处直接加载数据
|
||||
schedule();
|
||||
loadProjects();
|
||||
|
||||
+422
@@ -0,0 +1,422 @@
|
||||
/*!
|
||||
* NY 问卷考试系统数据平台 - 随机抽取页逻辑
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const $ = s => document.querySelector(s);
|
||||
const $$ = s => Array.prototype.slice.call(document.querySelectorAll(s));
|
||||
|
||||
function esc(s) {
|
||||
return String(s == null ? '' : s).replace(/[&<>"']/g, c =>
|
||||
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||
}
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
const LS_RESULT = 'ny_draw_result'; // 最近一次抽取结果(刷新后仍可下载)
|
||||
const LS_HISTORY = 'ny_draw_history'; // 累计抽中的学号(用于"排除已抽中")
|
||||
const LS_SETTINGS = 'ny_draw_settings'; // 来源 / 项目 / 班级 / 组别 / 人数
|
||||
|
||||
const state = {
|
||||
roster: [],
|
||||
projects: [],
|
||||
answers: [],
|
||||
source: 'roster',
|
||||
picked: [],
|
||||
history: new Set(),
|
||||
drawing: false,
|
||||
};
|
||||
|
||||
/* ---------------- 接口 ---------------- */
|
||||
async function api(action, params) {
|
||||
const q = new URLSearchParams(Object.assign({ action: action }, params || {}));
|
||||
const r = await fetch('api.php?' + q.toString(), { credentials: 'same-origin' });
|
||||
let j;
|
||||
try {
|
||||
j = await r.json();
|
||||
} catch (e) {
|
||||
throw new Error('接口返回异常(HTTP ' + r.status + ')');
|
||||
}
|
||||
if (r.status === 401 || (j && j.ok === false && /未登录/.test(j.msg || ''))) {
|
||||
location.href = 'login.php';
|
||||
throw new Error('未登录');
|
||||
}
|
||||
if (!j || !j.ok) {
|
||||
throw new Error((j && j.msg) || '请求失败');
|
||||
}
|
||||
return j.data;
|
||||
}
|
||||
|
||||
function showAlert(msg, type) {
|
||||
const el = $('#alert');
|
||||
if (!el) return;
|
||||
if (!msg) { el.classList.add('hidden'); return; }
|
||||
el.textContent = msg;
|
||||
el.className = 'alert' + (type === 'info' ? ' info' : '');
|
||||
}
|
||||
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
async function loadRoster() {
|
||||
const d = await api('roster');
|
||||
state.roster = (d && d.students ? d.students : []).map(s => ({
|
||||
sid: String(s.sid || ''),
|
||||
name: String(s.name || ''),
|
||||
cls: String(s.cls || ''),
|
||||
grp: String(s.grp || ''),
|
||||
}));
|
||||
const badge = $('#rosterBadge');
|
||||
if (badge) badge.textContent = state.roster.length;
|
||||
}
|
||||
|
||||
async function loadProjects() {
|
||||
try {
|
||||
state.projects = (await api('projects')) || [];
|
||||
} catch (e) {
|
||||
state.projects = [];
|
||||
}
|
||||
const sel = $('#projectSel');
|
||||
if (!sel) return;
|
||||
|
||||
const isFolder = p => (p.mode || '').toLowerCase().indexOf('folder') >= 0;
|
||||
const parentIds = new Set();
|
||||
state.projects.forEach(p => {
|
||||
const pid = String(p.parent_id || '0');
|
||||
if (pid !== '0' && pid !== '') parentIds.add(pid);
|
||||
});
|
||||
const folderSet = new Set();
|
||||
state.projects.forEach(p => { if (isFolder(p) || parentIds.has(p.id)) folderSet.add(p.id); });
|
||||
|
||||
const nameOf = id => {
|
||||
const f = state.projects.filter(p => p.id === id)[0];
|
||||
return f ? String(f.name || '') : '';
|
||||
};
|
||||
|
||||
const items = state.projects.filter(p => !folderSet.has(p.id));
|
||||
if (!items.length) {
|
||||
sel.innerHTML = '<option value="">(无可用的问卷 / 考试)</option>';
|
||||
return;
|
||||
}
|
||||
sel.innerHTML = items.map(p => {
|
||||
const pid = String(p.parent_id || '0');
|
||||
const fn = (pid !== '0' && pid !== '' && folderSet.has(pid)) ? nameOf(pid) : '';
|
||||
const label = fn ? fn + ' / ' + p.name : p.name;
|
||||
return '<option value="' + esc(p.id) + '">' + esc(label) + '</option>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function loadAnswers(pid) {
|
||||
const d = await api('data', { projectId: pid });
|
||||
state.answers = ((d && d.rows) ? d.rows : []).map(r => ({
|
||||
sid: String(r.sid || ''),
|
||||
name: String(r.name || ''),
|
||||
cls: String(r.cls || ''),
|
||||
grp: String(r.grp || ''),
|
||||
}));
|
||||
}
|
||||
|
||||
/* ---------------- 候选池 ---------------- */
|
||||
function baseList() {
|
||||
return state.source === 'project' ? state.answers : state.roster;
|
||||
}
|
||||
|
||||
function currentPool() {
|
||||
let list = baseList().slice();
|
||||
const cls = $('#clsSel').value;
|
||||
const grp = $('#grpSel').value;
|
||||
if (cls) list = list.filter(x => (x.cls || '') === cls);
|
||||
if (grp) list = list.filter(x => (x.grp || '') === grp);
|
||||
if ($('#excludePicked').checked) {
|
||||
list = list.filter(x => !state.history.has(String(x.sid)));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
function uniqSorted(arr) {
|
||||
const seen = {};
|
||||
const out = [];
|
||||
arr.forEach(v => { if (v && !seen[v]) { seen[v] = 1; out.push(v); } });
|
||||
out.sort((a, b) => String(a).localeCompare(String(b), 'zh', { numeric: true }));
|
||||
return out;
|
||||
}
|
||||
|
||||
function fillSel(sel, values, allLabel) {
|
||||
if (!sel) return;
|
||||
const cur = sel.value;
|
||||
sel.innerHTML = '<option value="">' + esc(allLabel) + '</option>' +
|
||||
values.map(v => '<option value="' + esc(v) + '">' + esc(v) + '</option>').join('');
|
||||
sel.value = (values.indexOf(cur) >= 0) ? cur : '';
|
||||
}
|
||||
|
||||
function populateFilters() {
|
||||
const list = baseList();
|
||||
fillSel($('#clsSel'), uniqSorted(list.map(x => x.cls || '')), '全部班级');
|
||||
fillSel($('#grpSel'), uniqSorted(list.map(x => x.grp || '')), '全部组别');
|
||||
}
|
||||
|
||||
function renderStats() {
|
||||
const pool = currentPool();
|
||||
$('#dPool').textContent = pool.length;
|
||||
$('#dPicked').textContent = state.picked.length;
|
||||
$('#dHistory').textContent = state.history.size;
|
||||
$('#dSource').textContent = state.source === 'project' ? '已提交人员' : '学号名单';
|
||||
|
||||
const cls = $('#clsSel').value, grp = $('#grpSel').value;
|
||||
const parts = [];
|
||||
if (cls) parts.push(cls);
|
||||
if (grp) parts.push(grp + '组');
|
||||
$('#drawHint').textContent = '候选 ' + pool.length + ' 人' +
|
||||
(parts.length ? '(' + parts.join(' / ') + ')' : '');
|
||||
}
|
||||
|
||||
/* ---------------- 抽取 ---------------- */
|
||||
function shuffle(arr) {
|
||||
const a = arr.slice();
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
const t = a[i]; a[i] = a[j]; a[j] = t;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
async function doDraw() {
|
||||
if (state.drawing) return;
|
||||
const pool = currentPool();
|
||||
if (!pool.length) {
|
||||
showAlert('当前筛选下没有可抽取的人员(可关闭「排除已抽中」或调整班级 / 组别)');
|
||||
return;
|
||||
}
|
||||
let n = parseInt($('#numInput').value, 10);
|
||||
if (!n || n < 1) n = 1;
|
||||
if (n > pool.length) n = pool.length;
|
||||
|
||||
state.drawing = true;
|
||||
const stage = $('#drawStage');
|
||||
const btn = $('#btnDraw');
|
||||
btn.disabled = true;
|
||||
showAlert('');
|
||||
|
||||
// 滚动动画:先快速闪动若干次
|
||||
stage.className = 'draw-stage rolling';
|
||||
const rounds = Math.min(24, 10 + n);
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
const p = pool[Math.floor(Math.random() * pool.length)];
|
||||
stage.textContent = p.name || p.sid || '—';
|
||||
await sleep(40 + i * 7);
|
||||
}
|
||||
|
||||
const picked = shuffle(pool).slice(0, n);
|
||||
state.picked = picked;
|
||||
picked.forEach(m => state.history.add(String(m.sid)));
|
||||
|
||||
stage.className = 'draw-stage done';
|
||||
stage.textContent = n === 1
|
||||
? (picked[0].name || picked[0].sid)
|
||||
: ('🎉 已抽取 ' + n + ' 人');
|
||||
|
||||
renderResult();
|
||||
renderStats();
|
||||
saveState();
|
||||
|
||||
state.drawing = false;
|
||||
btn.disabled = false;
|
||||
showAlert('已抽取 ' + n + ' 人' + (n > 1 ? ',可点击右侧按钮下载白名单' : ''), 'info');
|
||||
}
|
||||
|
||||
function renderResult() {
|
||||
const tb = $('#drawBody');
|
||||
if (!state.picked.length) {
|
||||
tb.innerHTML = '<tr><td colspan="5" class="empty">尚未抽取</td></tr>';
|
||||
return;
|
||||
}
|
||||
tb.innerHTML = state.picked.map((m, i) =>
|
||||
'<tr>' +
|
||||
'<td class="num">' + (i + 1) + '</td>' +
|
||||
'<td class="sid">' + esc(m.sid) + '</td>' +
|
||||
'<td class="name">' + esc(m.name) + '</td>' +
|
||||
'<td>' + esc(m.cls || '') + '</td>' +
|
||||
'<td>' + esc(m.grp || '') + '</td>' +
|
||||
'</tr>').join('');
|
||||
}
|
||||
|
||||
/* ---------------- 下载 ---------------- */
|
||||
async function download(type) {
|
||||
if (!state.picked.length) {
|
||||
showAlert('请先点击「🎲 开始抽取」');
|
||||
return;
|
||||
}
|
||||
const fname = type === 'import' ? '白名单导入模板.xlsx' : '白名单列表.xlsx';
|
||||
try {
|
||||
const r = await fetch('api.php?action=draw_export', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
type: type,
|
||||
members: state.picked.map(m => ({ name: m.name, sid: m.sid })),
|
||||
}),
|
||||
});
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
const blob = await r.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fname;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 3000);
|
||||
showAlert('已下载 ' + fname, 'info');
|
||||
} catch (e) {
|
||||
showAlert('导出失败:' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 本地缓存 ---------------- */
|
||||
function saveState() {
|
||||
try {
|
||||
localStorage.setItem(LS_RESULT, JSON.stringify(state.picked));
|
||||
localStorage.setItem(LS_HISTORY, JSON.stringify(Array.from(state.history)));
|
||||
} catch (e) { /* 忽略 */ }
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
try {
|
||||
localStorage.setItem(LS_SETTINGS, JSON.stringify({
|
||||
source: state.source,
|
||||
projectId: $('#projectSel').value,
|
||||
cls: $('#clsSel').value,
|
||||
grp: $('#grpSel').value,
|
||||
num: $('#numInput').value,
|
||||
exclude: $('#excludePicked').checked,
|
||||
}));
|
||||
} catch (e) { /* 忽略 */ }
|
||||
}
|
||||
|
||||
function loadState() {
|
||||
try {
|
||||
const p = JSON.parse(localStorage.getItem(LS_RESULT) || '[]');
|
||||
if (Array.isArray(p)) state.picked = p;
|
||||
const h = JSON.parse(localStorage.getItem(LS_HISTORY) || '[]');
|
||||
if (Array.isArray(h)) state.history = new Set(h.map(String));
|
||||
} catch (e) { /* 忽略 */ }
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
try {
|
||||
const s = JSON.parse(localStorage.getItem(LS_SETTINGS) || '{}');
|
||||
if (s && typeof s === 'object') return s;
|
||||
} catch (e) { /* 忽略 */ }
|
||||
return {};
|
||||
}
|
||||
|
||||
/* ---------------- 事件 ---------------- */
|
||||
function bind() {
|
||||
$('#sourceSel').addEventListener('change', async () => {
|
||||
state.source = $('#sourceSel').value;
|
||||
$('#projectField').classList.toggle('hidden', state.source !== 'project');
|
||||
state.answers = [];
|
||||
state.picked = [];
|
||||
renderResult();
|
||||
if (state.source === 'project') {
|
||||
if (!state.projects.length) await loadProjects();
|
||||
const pid = $('#projectSel').value;
|
||||
if (pid) {
|
||||
try { await loadAnswers(pid); } catch (e) { showAlert(e.message); }
|
||||
}
|
||||
}
|
||||
populateFilters();
|
||||
renderStats();
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
$('#projectSel').addEventListener('change', async () => {
|
||||
const pid = $('#projectSel').value;
|
||||
if (!pid) { state.answers = []; populateFilters(); renderStats(); return; }
|
||||
showAlert('正在拉取已提交人员…', 'info');
|
||||
try {
|
||||
await loadAnswers(pid);
|
||||
showAlert('');
|
||||
} catch (e) {
|
||||
showAlert(e.message);
|
||||
}
|
||||
populateFilters();
|
||||
renderStats();
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
['#clsSel', '#grpSel'].forEach(id => {
|
||||
$(id).addEventListener('change', () => { renderStats(); saveSettings(); });
|
||||
});
|
||||
$('#numInput').addEventListener('change', saveSettings);
|
||||
$('#excludePicked').addEventListener('change', () => { renderStats(); saveSettings(); });
|
||||
|
||||
$$('[data-num]').forEach(b => {
|
||||
b.addEventListener('click', () => {
|
||||
const v = b.dataset.num;
|
||||
if (v === 'all') {
|
||||
const pool = currentPool();
|
||||
$('#numInput').value = String(Math.max(1, pool.length));
|
||||
} else {
|
||||
$('#numInput').value = v;
|
||||
}
|
||||
saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
$('#btnDraw').addEventListener('click', doDraw);
|
||||
$('#btnReset').addEventListener('click', () => {
|
||||
state.picked = [];
|
||||
renderResult();
|
||||
renderStats();
|
||||
saveState();
|
||||
const stage = $('#drawStage');
|
||||
stage.className = 'draw-stage';
|
||||
stage.textContent = '点击「🎲 开始抽取」';
|
||||
showAlert('');
|
||||
});
|
||||
$('#btnClearHistory').addEventListener('click', () => {
|
||||
state.history = new Set();
|
||||
state.picked = [];
|
||||
renderResult();
|
||||
renderStats();
|
||||
saveState();
|
||||
showAlert('已清空累计抽取记录', 'info');
|
||||
});
|
||||
$('#btnDlImport').addEventListener('click', () => download('import'));
|
||||
$('#btnDlList').addEventListener('click', () => download('list'));
|
||||
}
|
||||
|
||||
/* ---------------- 启动 ---------------- */
|
||||
document.addEventListener('DOMContentLoaded', async function () {
|
||||
loadState();
|
||||
const s = loadSettings();
|
||||
if (s.exclude === false) $('#excludePicked').checked = false;
|
||||
if (s.num) $('#numInput').value = s.num;
|
||||
if (s.source === 'project') {
|
||||
state.source = 'project';
|
||||
$('#sourceSel').value = 'project';
|
||||
$('#projectField').classList.remove('hidden');
|
||||
}
|
||||
|
||||
bind();
|
||||
renderResult();
|
||||
|
||||
try {
|
||||
await loadRoster();
|
||||
} catch (e) {
|
||||
showAlert('名单加载失败:' + e.message);
|
||||
}
|
||||
if (state.source === 'project') {
|
||||
await loadProjects();
|
||||
const pid = s.projectId || $('#projectSel').value;
|
||||
if (pid) {
|
||||
$('#projectSel').value = pid;
|
||||
try { await loadAnswers(pid); } catch (e) { /* 静默 */ }
|
||||
}
|
||||
}
|
||||
populateFilters();
|
||||
if (s.cls) $('#clsSel').value = s.cls;
|
||||
if (s.grp) $('#grpSel').value = s.grp;
|
||||
renderStats();
|
||||
});
|
||||
})();
|
||||
@@ -585,3 +585,36 @@ body.dash.fs-on .group-compare { gap: 12px; }
|
||||
body.dash.fs-on .gpanel { padding: 12px 14px; }
|
||||
body.dash.fs-on .member-list { max-height: 30vh; }
|
||||
|
||||
|
||||
/* ================= 随机抽取页 ================= */
|
||||
.panel-head .actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
|
||||
/* 抽取人数输入框:在 toolbar 内联按钮组中收窄 */
|
||||
.field .inline input[type=number] { width: 78px; flex: none; }
|
||||
|
||||
.draw-stage {
|
||||
margin: 18px; padding: 26px 18px;
|
||||
border: 1px dashed var(--border); border-radius: var(--radius);
|
||||
background: linear-gradient(180deg, #fbfcff, #f6f8ff);
|
||||
text-align: center; font-size: 20px; font-weight: 700;
|
||||
color: var(--text-3); letter-spacing: 1px;
|
||||
min-height: 86px; display: flex; align-items: center; justify-content: center;
|
||||
transition: color .2s ease, background .2s ease;
|
||||
}
|
||||
.draw-stage.rolling {
|
||||
color: var(--primary);
|
||||
background: linear-gradient(180deg, #f2f6ff, #eaf0ff);
|
||||
animation: drawShake .12s infinite alternate;
|
||||
}
|
||||
.draw-stage.done {
|
||||
color: var(--ok);
|
||||
background: linear-gradient(180deg, #f2fbf5, #e9f7ee);
|
||||
font-size: 26px;
|
||||
}
|
||||
@keyframes drawShake {
|
||||
from { transform: translateY(-1px) scale(.995); }
|
||||
to { transform: translateY(1px) scale(1.005); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.draw-stage.rolling { animation: none; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user