mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
ui: add hint on problem_files page
This commit is contained in:
1 parent
de375f74c7
commit
8b9ff434e2
10 files changed
+157
-148
No files matched your search
+7
-7
@@ -28,18 +28,18 @@
|
||||
"license": "AGPL-3.0-only",
|
||||
"devDependencies": {
|
||||
"@types/cross-spawn": "^6.0.2",
|
||||
"@types/node": "^16.10.3",
|
||||
"@types/node": "^16.10.9",
|
||||
"@types/semver": "^7.3.8",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@typescript-eslint/parser": "^4.33.0",
|
||||
"cac": "^6.7.8",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"cac": "^6.7.11",
|
||||
"cross-env": "^7.0.3",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"esbuild": "^0.13.4",
|
||||
"eslint": "^7.32.0",
|
||||
"esbuild": "^0.13.6",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-airbnb-typescript": "12.3.1",
|
||||
"eslint-import-resolver-typescript": "^2.5.0",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-import": "^2.25.2",
|
||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"globby": "11.0.4",
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
"ws": "^8.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ws": "^7.4.7"
|
||||
"@types/ws": "^8.2.0"
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,7 @@ module.exports = {
|
||||
'always',
|
||||
{ exceptAfterSingleLine: true },
|
||||
],
|
||||
|
||||
'lines-between-class-members': [
|
||||
'error',
|
||||
'always',
|
||||
@@ -90,6 +91,7 @@ module.exports = {
|
||||
'import/extensions': 'off',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'max-classes-per-file': 'off',
|
||||
'newline-per-chained-call': 'off',
|
||||
'no-empty': ['warn', { allowEmptyCatch: true }],
|
||||
'no-console': 'off',
|
||||
'no-continue': 'off',
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function createHint(message: string, element?: any) {
|
||||
a.onclick = () => {
|
||||
new InfoDialog({
|
||||
cancelByClickingBack: false,
|
||||
$body: tpl.typoMsg(i18n(message)),
|
||||
$body: tpl.typoMsg(i18n(message), true),
|
||||
}).open();
|
||||
};
|
||||
$(element).append(a);
|
||||
|
||||
@@ -110,7 +110,7 @@ class WikiAboutHandler extends Handler {
|
||||
}
|
||||
|
||||
class SetThemeHandler extends Handler {
|
||||
noCheckPermView = true
|
||||
noCheckPermView = true;
|
||||
|
||||
async get({ theme }) {
|
||||
await user.setById(this.user._id, { theme });
|
||||
|
||||
@@ -341,6 +341,7 @@ Highlight discussions: 高亮讨论
|
||||
Highlight: 高亮
|
||||
Hint: 提示
|
||||
Hint::icon::difficulty: Hydro 中题目的难度,根据递交数、通过率以及每个递交的递交时间和评测结果,综合计算得出,可能不准确。
|
||||
Hint::icon::testdata: <a href="https://hydro.js.org/docs/user/testdata/" target="_blank">请参照文档说明</a>
|
||||
Hint::page::main: 欢迎来到 Hydro! 您可在右上角注册或登录。此类提示仅会显示一次。
|
||||
Hint::page::problem_detail: 这是题目详情页面。您可点击右侧“进入在线编程模式"开始编写您的代码。
|
||||
Hint::page::problem_files: 您可以将文件或压缩包拖拽至对应区域来上传文件。点击文件名在线编辑或按住 Ctrl 后点击文件名下载单个文件。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/ui-default",
|
||||
"version": "4.25.4",
|
||||
"version": "4.25.5",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "hydro.js",
|
||||
@@ -13,7 +13,7 @@
|
||||
"@blueprintjs/core": "^3.51.0",
|
||||
"@blueprintjs/icons": "^3.30.2",
|
||||
"@hydrooj/utils": "workspace:*",
|
||||
"@types/jquery": "^3.5.6",
|
||||
"@types/jquery": "^3.5.7",
|
||||
"@types/json-schema": "^7.0.9",
|
||||
"@types/katex": "^0.11.1",
|
||||
"@types/sockjs-client": "^1.5.1",
|
||||
@@ -31,10 +31,10 @@
|
||||
"emojis-keywords": "2.0.0",
|
||||
"emojis-list": "2.1.0",
|
||||
"esbuild-loader": "^2.16.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-import-resolver-webpack": "^0.13.1",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-import": "^2.25.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-react": "^7.26.1",
|
||||
"fancy-log": "^1.3.3",
|
||||
@@ -53,9 +53,9 @@
|
||||
"jquery.transit": "^0.9.12",
|
||||
"mini-css-extract-plugin": "^1.6.2",
|
||||
"moment": "^2.29.1",
|
||||
"monaco-editor": "^0.29.0",
|
||||
"monaco-editor": "^0.29.1",
|
||||
"monaco-editor-webpack-plugin": "^4.2.0",
|
||||
"nanoid": "^3.1.29",
|
||||
"nanoid": "^3.1.30",
|
||||
"normalize.css": "^8.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"pickadate": "^3.6.4",
|
||||
@@ -90,19 +90,19 @@
|
||||
"wastyle": "^0.0.5",
|
||||
"web-streams-polyfill": "^3.1.1",
|
||||
"webpack": "^4.46.0",
|
||||
"webpack-bundle-analyzer": "^4.4.2",
|
||||
"webpack-bundle-analyzer": "^4.5.0",
|
||||
"webpack-dev-server": "^3.11.2",
|
||||
"webpackbar": "^5.0.0-3"
|
||||
},
|
||||
"dependencies": {
|
||||
"esbuild": "^0.13.4",
|
||||
"esbuild": "^0.13.6",
|
||||
"fs-extra": "^10.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsesc": "^3.0.2",
|
||||
"katex": "^0.13.18",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^12.2.0",
|
||||
"markdown-it-anchor": "^8.3.1",
|
||||
"markdown-it-anchor": "^8.4.1",
|
||||
"markdown-it-footnote": "^3.0.3",
|
||||
"markdown-it-imsize": "^2.0.1",
|
||||
"markdown-it-mark": "^3.0.1",
|
||||
|
||||
@@ -12,146 +12,146 @@ function onBeforeUnload(e) {
|
||||
e.returnValue = '';
|
||||
}
|
||||
|
||||
const page = new NamedPage('home_files', () => {
|
||||
function ensureAndGetSelectedFiles() {
|
||||
const files = _.map(
|
||||
$('.home-files tbody [data-checkbox-group="user_files"]:checked'),
|
||||
(ch) => $(ch).closest('tr').attr('data-filename'),
|
||||
);
|
||||
if (files.length === 0) {
|
||||
Notification.error(i18n('Please select at least one file to perform this operation.'));
|
||||
return null;
|
||||
}
|
||||
return files;
|
||||
function ensureAndGetSelectedFiles() {
|
||||
const files = _.map(
|
||||
$('.home-files tbody [data-checkbox-group="user_files"]:checked'),
|
||||
(ch) => $(ch).closest('tr').attr('data-filename'),
|
||||
);
|
||||
if (files.length === 0) {
|
||||
Notification.error(i18n('Please select at least one file to perform this operation.'));
|
||||
return null;
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
async function handleClickUpload(files) {
|
||||
if (!files) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = true;
|
||||
input.click();
|
||||
await new Promise((resolve) => { input.onchange = resolve; });
|
||||
files = input.files;
|
||||
}
|
||||
if (!files.length) {
|
||||
Notification.warn(i18n('No file selected.'));
|
||||
return;
|
||||
}
|
||||
const dialog = new Dialog({
|
||||
$body: `
|
||||
<div class="file-label" style="text-align: center; margin-bottom: 5px; color: gray; font-size: small;"></div>
|
||||
<div class="bp3-progress-bar bp3-intent-primary bp3-no-stripes">
|
||||
<div class="file-progress bp3-progress-meter" style="width: 0"></div>
|
||||
</div>
|
||||
<div class="upload-label" style="text-align: center; margin: 5px 0; color: gray; font-size: small;"></div>
|
||||
<div class="bp3-progress-bar bp3-intent-primary bp3-no-stripes">
|
||||
<div class="upload-progress bp3-progress-meter" style="width: 0"></div>
|
||||
</div>`,
|
||||
});
|
||||
try {
|
||||
Notification.info(i18n('Uploading files...'));
|
||||
window.addEventListener('beforeunload', onBeforeUnload);
|
||||
dialog.open();
|
||||
const $uploadLabel = dialog.$dom.find('.dialog__body .upload-label');
|
||||
const $uploadProgress = dialog.$dom.find('.dialog__body .upload-progress');
|
||||
const $fileLabel = dialog.$dom.find('.dialog__body .file-label');
|
||||
const $fileProgress = dialog.$dom.find('.dialog__body .file-progress');
|
||||
for (const i in files) {
|
||||
if (Number.isNaN(+i)) continue;
|
||||
const file = files[i];
|
||||
const data = new FormData();
|
||||
data.append('filename', file.name);
|
||||
data.append('file', file);
|
||||
data.append('operation', 'upload_file');
|
||||
await request.postFile('', data, {
|
||||
xhr() {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.upload.addEventListener('loadstart', () => {
|
||||
$fileLabel.text(`[${+i + 1}/${files.length}] ${file.name}`);
|
||||
$fileProgress.width(`${Math.round((+i + 1) / files.length * 100)}%`);
|
||||
$uploadLabel.text(i18n('Uploading... ({0}%)', 0));
|
||||
$uploadProgress.width(0);
|
||||
});
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const percentComplete = Math.round((e.loaded / e.total) * 100);
|
||||
if (percentComplete === 100) $uploadLabel.text(i18n('Processing...'));
|
||||
else $uploadLabel.text(i18n('Uploading... ({0}%)', percentComplete));
|
||||
$uploadProgress.width(`${percentComplete}%`);
|
||||
}
|
||||
}, false);
|
||||
return xhr;
|
||||
},
|
||||
});
|
||||
}
|
||||
window.removeEventListener('beforeunload', onBeforeUnload);
|
||||
Notification.success(i18n('File uploaded successfully.'));
|
||||
await pjax.request({ push: false });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Notification.error(i18n('File upload failed: {0}', e.toString()));
|
||||
} finally {
|
||||
dialog.close();
|
||||
}
|
||||
async function handleClickUpload(files) {
|
||||
if (!files) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = true;
|
||||
input.click();
|
||||
await new Promise((resolve) => { input.onchange = resolve; });
|
||||
files = input.files;
|
||||
}
|
||||
|
||||
async function handleClickRemoveSelected() {
|
||||
const selectedFiles = ensureAndGetSelectedFiles();
|
||||
if (selectedFiles === null) return;
|
||||
const action = await new ConfirmDialog({
|
||||
$body: tpl`
|
||||
<div class="typo">
|
||||
<p>${i18n('Confirm to delete the selected files?')}</p>
|
||||
</div>`,
|
||||
}).open();
|
||||
if (action !== 'yes') return;
|
||||
try {
|
||||
await request.post('', {
|
||||
operation: 'delete_files',
|
||||
files: selectedFiles,
|
||||
if (!files.length) {
|
||||
Notification.warn(i18n('No file selected.'));
|
||||
return;
|
||||
}
|
||||
const dialog = new Dialog({
|
||||
$body: `
|
||||
<div class="file-label" style="text-align: center; margin-bottom: 5px; color: gray; font-size: small;"></div>
|
||||
<div class="bp3-progress-bar bp3-intent-primary bp3-no-stripes">
|
||||
<div class="file-progress bp3-progress-meter" style="width: 0"></div>
|
||||
</div>
|
||||
<div class="upload-label" style="text-align: center; margin: 5px 0; color: gray; font-size: small;"></div>
|
||||
<div class="bp3-progress-bar bp3-intent-primary bp3-no-stripes">
|
||||
<div class="upload-progress bp3-progress-meter" style="width: 0"></div>
|
||||
</div>`,
|
||||
});
|
||||
try {
|
||||
Notification.info(i18n('Uploading files...'));
|
||||
window.addEventListener('beforeunload', onBeforeUnload);
|
||||
dialog.open();
|
||||
const $uploadLabel = dialog.$dom.find('.dialog__body .upload-label');
|
||||
const $uploadProgress = dialog.$dom.find('.dialog__body .upload-progress');
|
||||
const $fileLabel = dialog.$dom.find('.dialog__body .file-label');
|
||||
const $fileProgress = dialog.$dom.find('.dialog__body .file-progress');
|
||||
for (const i in files) {
|
||||
if (Number.isNaN(+i)) continue;
|
||||
const file = files[i];
|
||||
const data = new FormData();
|
||||
data.append('filename', file.name);
|
||||
data.append('file', file);
|
||||
data.append('operation', 'upload_file');
|
||||
await request.postFile('', data, {
|
||||
xhr() {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.upload.addEventListener('loadstart', () => {
|
||||
$fileLabel.text(`[${+i + 1}/${files.length}] ${file.name}`);
|
||||
$fileProgress.width(`${Math.round((+i + 1) / files.length * 100)}%`);
|
||||
$uploadLabel.text(i18n('Uploading... ({0}%)', 0));
|
||||
$uploadProgress.width(0);
|
||||
});
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const percentComplete = Math.round((e.loaded / e.total) * 100);
|
||||
if (percentComplete === 100) $uploadLabel.text(i18n('Processing...'));
|
||||
else $uploadLabel.text(i18n('Uploading... ({0}%)', percentComplete));
|
||||
$uploadProgress.width(`${percentComplete}%`);
|
||||
}
|
||||
}, false);
|
||||
return xhr;
|
||||
},
|
||||
});
|
||||
Notification.success(i18n('Selected files have been deleted.'));
|
||||
await pjax.request({ push: false });
|
||||
} catch (error) {
|
||||
Notification.error(error.message);
|
||||
}
|
||||
window.removeEventListener('beforeunload', onBeforeUnload);
|
||||
Notification.success(i18n('File uploaded successfully.'));
|
||||
await pjax.request({ push: false });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Notification.error(i18n('File upload failed: {0}', e.toString()));
|
||||
} finally {
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JQuery.DragOverEvent<HTMLElement, undefined, HTMLElement, HTMLElement>} ev
|
||||
*/
|
||||
function handleDragOver(ev) {
|
||||
ev.preventDefault();
|
||||
// TODO display a drag-drop allowed hint
|
||||
async function handleClickRemoveSelected() {
|
||||
const selectedFiles = ensureAndGetSelectedFiles();
|
||||
if (selectedFiles === null) return;
|
||||
const action = await new ConfirmDialog({
|
||||
$body: tpl`
|
||||
<div class="typo">
|
||||
<p>${i18n('Confirm to delete the selected files?')}</p>
|
||||
</div>`,
|
||||
}).open();
|
||||
if (action !== 'yes') return;
|
||||
try {
|
||||
await request.post('', {
|
||||
operation: 'delete_files',
|
||||
files: selectedFiles,
|
||||
});
|
||||
Notification.success(i18n('Selected files have been deleted.'));
|
||||
await pjax.request({ push: false });
|
||||
} catch (error) {
|
||||
Notification.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JQuery.DropEvent<HTMLElement, undefined, HTMLElement, HTMLElement>} ev
|
||||
*/
|
||||
function handleDrop(ev) {
|
||||
ev.preventDefault();
|
||||
if (!$('[name="upload_testdata"]').length) {
|
||||
Notification.error(i18n("You don't have permission to upload file."));
|
||||
return;
|
||||
}
|
||||
ev = ev.originalEvent;
|
||||
const files = [];
|
||||
if (ev.dataTransfer.items) {
|
||||
for (let i = 0; i < ev.dataTransfer.items.length; i++) {
|
||||
if (ev.dataTransfer.items[i].kind === 'file') {
|
||||
const file = ev.dataTransfer.items[i].getAsFile();
|
||||
files.push(file);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < ev.dataTransfer.files.length; i++) {
|
||||
files.push(ev.dataTransfer.files[i]);
|
||||
/**
|
||||
* @param {JQuery.DragOverEvent<HTMLElement, undefined, HTMLElement, HTMLElement>} ev
|
||||
*/
|
||||
function handleDragOver(ev) {
|
||||
ev.preventDefault();
|
||||
// TODO display a drag-drop allowed hint
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JQuery.DropEvent<HTMLElement, undefined, HTMLElement, HTMLElement>} ev
|
||||
*/
|
||||
function handleDrop(ev) {
|
||||
ev.preventDefault();
|
||||
if (!$('[name="upload_testdata"]').length) {
|
||||
Notification.error(i18n("You don't have permission to upload file."));
|
||||
return;
|
||||
}
|
||||
ev = ev.originalEvent;
|
||||
const files = [];
|
||||
if (ev.dataTransfer.items) {
|
||||
for (let i = 0; i < ev.dataTransfer.items.length; i++) {
|
||||
if (ev.dataTransfer.items[i].kind === 'file') {
|
||||
const file = ev.dataTransfer.items[i].getAsFile();
|
||||
files.push(file);
|
||||
}
|
||||
}
|
||||
handleClickUpload(files);
|
||||
} else {
|
||||
for (let i = 0; i < ev.dataTransfer.files.length; i++) {
|
||||
files.push(ev.dataTransfer.files[i]);
|
||||
}
|
||||
}
|
||||
handleClickUpload(files);
|
||||
}
|
||||
|
||||
const page = new NamedPage('home_files', () => {
|
||||
const clip = new Clipboard('.home-files .col--name', {
|
||||
text: (trigger) => {
|
||||
const filename = trigger.closest('[data-filename]').getAttribute('data-filename');
|
||||
|
||||
@@ -3,6 +3,7 @@ import { NamedPage } from 'vj/misc/Page';
|
||||
import Notification from 'vj/components/notification';
|
||||
import { ConfirmDialog, ActionDialog, Dialog } from 'vj/components/dialog/index';
|
||||
import download from 'vj/components/zipDownloader';
|
||||
import createHint from 'vj/components/hint';
|
||||
import request from 'vj/utils/request';
|
||||
import pjax from 'vj/utils/pjax';
|
||||
import tpl from 'vj/utils/tpl';
|
||||
@@ -210,6 +211,7 @@ const page = new NamedPage('problem_files', () => {
|
||||
? ev.currentTarget.closest('[data-filename]').getAttribute('data-filename')
|
||||
// eslint-disable-next-line no-alert
|
||||
: prompt('Filename');
|
||||
if (!filename) return;
|
||||
const filesize = ev
|
||||
? +ev.currentTarget.closest('[data-size]').getAttribute('data-size')
|
||||
: 0;
|
||||
@@ -251,6 +253,10 @@ const page = new NamedPage('problem_files', () => {
|
||||
$(document).on('drop', '.problem-files-additional_file', (ev) => handleDrop('additional_file', ev));
|
||||
$(document).on('click', '[name="download_selected_testdata"]', () => handleClickDownloadSelected('testdata'));
|
||||
$(document).on('click', '[name="download_selected_file"]', () => handleClickDownloadSelected('additional_file'));
|
||||
$(document).on('vjContentNew', (e) => {
|
||||
createHint('Hint::icon::testdata', $(e.target).find('[name="create_testdata"]').get(0).parentNode.parentNode.children[0]);
|
||||
});
|
||||
createHint('Hint::icon::testdata', $(document).find('[name="create_testdata"]').get(0).parentNode.parentNode.children[0]);
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -13,10 +13,10 @@ export default function tpl(pieces, ...substitutions) {
|
||||
return result;
|
||||
}
|
||||
|
||||
tpl.typoMsg = function (msg) {
|
||||
tpl.typoMsg = function (msg, raw = false) {
|
||||
return tpl`
|
||||
<div class="typo">
|
||||
<p>${msg}</p>
|
||||
<p>${raw ? { html: msg, templateRaw: true } : msg}</p>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user