mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
core&ui: enhanced category selector
This commit is contained in:
1 parent
74f4e44f0f
commit
6567c739b6
12 files changed
+45
-54
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydrooj",
|
"name": "hydrooj",
|
||||||
"version": "3.13.1",
|
"version": "3.13.2",
|
||||||
"bin": "bin/hydrooj.js",
|
"bin": "bin/hydrooj.js",
|
||||||
"main": "src/loader",
|
"main": "src/loader",
|
||||||
"module": "src/loader",
|
"module": "src/loader",
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Handler, Route } from '../service/server';
|
||||||
|
|
||||||
|
class ProblemCategoryCompatHandler extends Handler {
|
||||||
|
async get({ category }) {
|
||||||
|
this.response.redirect = this.url('problem_main', { query: { q: `category:${category}` } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
global.Hydro.handler.compat = () => {
|
||||||
|
Route('problem_category_compat', '/p/category/:category', ProblemCategoryCompatHandler);
|
||||||
|
};
|
||||||
@@ -14,3 +14,4 @@ import './domain';
|
|||||||
import './status';
|
import './status';
|
||||||
import './api';
|
import './api';
|
||||||
import './blog';
|
import './blog';
|
||||||
|
import './compat';
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import AdmZip from 'adm-zip';
|
import AdmZip from 'adm-zip';
|
||||||
import { readFile, statSync } from 'fs-extra';
|
import { readFile, statSync } from 'fs-extra';
|
||||||
import { isBinaryFile } from 'isbinaryfile';
|
import { isBinaryFile } from 'isbinaryfile';
|
||||||
import { intersection, isSafeInteger } from 'lodash';
|
import { flattenDeep, intersection, isSafeInteger } from 'lodash';
|
||||||
import { FilterQuery, ObjectID } from 'mongodb';
|
import { FilterQuery, ObjectID } from 'mongodb';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { sortFiles, streamToBuffer } from '@hydrooj/utils/lib/utils';
|
import { sortFiles, streamToBuffer } from '@hydrooj/utils/lib/utils';
|
||||||
@@ -125,7 +125,7 @@ export class ProblemHandler extends Handler {
|
|||||||
page, ppcount, pcount, pdocs, psdict, qs,
|
page, ppcount, pcount, pdocs, psdict, qs,
|
||||||
}),
|
}),
|
||||||
this.renderHTML('partials/problem_stat.html', { pcount, pcountRelation }),
|
this.renderHTML('partials/problem_stat.html', { pcount, pcountRelation }),
|
||||||
this.renderHTML('partials/problem_lucky.html', { category }),
|
this.renderHTML('partials/problem_lucky.html', { qs }),
|
||||||
])).map((i) => ({ html: i })),
|
])).map((i) => ({ html: i })),
|
||||||
raw: {
|
raw: {
|
||||||
page, pcount, ppcount, pdocs, psdict, category,
|
page, pcount, ppcount, pdocs, psdict, category,
|
||||||
@@ -138,8 +138,7 @@ export class ProblemHandler extends Handler {
|
|||||||
export class ProblemMainHandler extends ProblemHandler {
|
export class ProblemMainHandler extends ProblemHandler {
|
||||||
@param('page', Types.PositiveInt, true)
|
@param('page', Types.PositiveInt, true)
|
||||||
@param('q', Types.Content, true)
|
@param('q', Types.Content, true)
|
||||||
@param('category', Types.Name, true, null, parseCategory)
|
async get(domainId: string, page = 1, q = '') {
|
||||||
async get(domainId: string, page = 1, q = '', category = []) {
|
|
||||||
this.response.template = 'problem_main.html';
|
this.response.template = 'problem_main.html';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
const query = buildQuery(this.user);
|
const query = buildQuery(this.user);
|
||||||
@@ -148,11 +147,15 @@ export class ProblemMainHandler extends ProblemHandler {
|
|||||||
let sort: string[];
|
let sort: string[];
|
||||||
let fail = false;
|
let fail = false;
|
||||||
let pcountRelation = 'eq';
|
let pcountRelation = 'eq';
|
||||||
|
const category = flattenDeep(decodeURIComponent(q).split(' ')
|
||||||
|
.filter((i) => i.startsWith('category:'))
|
||||||
|
.map((i) => i.split(':')[1]?.split(',')));
|
||||||
|
const text = q.split(' ').filter((i) => !i.startsWith('category:')).join(' ');
|
||||||
if (category.length) query.$and = category.map((tag) => ({ tag }));
|
if (category.length) query.$and = category.map((tag) => ({ tag }));
|
||||||
if (q) category.push(q);
|
if (text) category.push(text);
|
||||||
if (category.length) this.UiContext.extraTitleContent = category.join(',');
|
if (category.length) this.UiContext.extraTitleContent = category.join(',');
|
||||||
let total = 0;
|
let total = 0;
|
||||||
if (q) {
|
if (text) {
|
||||||
if (search) {
|
if (search) {
|
||||||
const result = await search(domainId, q, { skip: (page - 1) * system.get('pagination.problem') });
|
const result = await search(domainId, q, { skip: (page - 1) * system.get('pagination.problem') });
|
||||||
total = result.total;
|
total = result.total;
|
||||||
@@ -166,7 +169,7 @@ export class ProblemMainHandler extends ProblemHandler {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
sort = result.hits;
|
sort = result.hits;
|
||||||
} else query.$text = { $search: q };
|
} else query.$text = { $search: text };
|
||||||
}
|
}
|
||||||
await bus.serial('problem/list', query, this);
|
await bus.serial('problem/list', query, this);
|
||||||
// eslint-disable-next-line prefer-const
|
// eslint-disable-next-line prefer-const
|
||||||
@@ -209,7 +212,7 @@ export class ProblemMainHandler extends ProblemHandler {
|
|||||||
pdocs,
|
pdocs,
|
||||||
psdict,
|
psdict,
|
||||||
category: category.join(','),
|
category: category.join(','),
|
||||||
qs: q ? `q=${encodeURIComponent(q)}` : '',
|
qs: q,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,8 +284,12 @@ export class ProblemMainHandler extends ProblemHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ProblemRandomHandler extends ProblemHandler {
|
export class ProblemRandomHandler extends ProblemHandler {
|
||||||
@param('category', Types.Name, true, null, parseCategory)
|
@param('q', Types.Content, true)
|
||||||
async get(domainId: string, category: string[] = []) {
|
async get(domainId: string, qs = '') {
|
||||||
|
const category = flattenDeep(decodeURIComponent(qs).split(' ')
|
||||||
|
.filter((i) => i.startsWith('category:'))
|
||||||
|
.map((i) => i.split(':')[1]?.split(',')));
|
||||||
|
console.log(qs, category);
|
||||||
const q = buildQuery(this.user);
|
const q = buildQuery(this.user);
|
||||||
if (category.length) q.$and = category.map((tag) => ({ tag }));
|
if (category.length) q.$and = category.map((tag) => ({ tag }));
|
||||||
await bus.serial('problem/list', q, this);
|
await bus.serial('problem/list', q, this);
|
||||||
@@ -906,7 +913,6 @@ export class ProblemPrefixListHandler extends Handler {
|
|||||||
|
|
||||||
export async function apply() {
|
export async function apply() {
|
||||||
Route('problem_main', '/p', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM);
|
Route('problem_main', '/p', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM);
|
||||||
Route('problem_category', '/p/category/:category', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM);
|
|
||||||
Route('problem_random', '/problem/random', ProblemRandomHandler, PERM.PERM_VIEW_PROBLEM);
|
Route('problem_random', '/problem/random', ProblemRandomHandler, PERM.PERM_VIEW_PROBLEM);
|
||||||
Route('problem_detail', '/p/:pid', ProblemDetailHandler, PERM.PERM_VIEW_PROBLEM);
|
Route('problem_detail', '/p/:pid', ProblemDetailHandler, PERM.PERM_VIEW_PROBLEM);
|
||||||
Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM);
|
Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM);
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default (router, logger) => async (ctx: KoaContext, next) => {
|
|||||||
}
|
}
|
||||||
for (const key in kwargs.query || {}) {
|
for (const key in kwargs.query || {}) {
|
||||||
if (query[key] instanceof ObjectID) query[key] = kwargs.query[key].toHexString();
|
if (query[key] instanceof ObjectID) query[key] = kwargs.query[key].toHexString();
|
||||||
else query[key] = encodeURIComponent(kwargs.query[key].toString());
|
else query[key] = kwargs.query[key].toString();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const { anchor } = args;
|
const { anchor } = args;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/ui-default",
|
"name": "@hydrooj/ui-default",
|
||||||
"version": "4.36.1",
|
"version": "4.36.2",
|
||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "hydro.js",
|
"main": "hydro.js",
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import Dropdown from 'vj/components/dropdown/Dropdown';
|
|||||||
import createHint from 'vj/components/hint';
|
import createHint from 'vj/components/hint';
|
||||||
import { downloadProblemSet } from 'vj/components/zipDownloader';
|
import { downloadProblemSet } from 'vj/components/zipDownloader';
|
||||||
import pjax from 'vj/utils/pjax';
|
import pjax from 'vj/utils/pjax';
|
||||||
import substitute from 'vj/utils/substitute';
|
|
||||||
import request from 'vj/utils/request';
|
import request from 'vj/utils/request';
|
||||||
import tpl from 'vj/utils/tpl';
|
import tpl from 'vj/utils/tpl';
|
||||||
import delay from 'vj/utils/delay';
|
import delay from 'vj/utils/delay';
|
||||||
@@ -52,19 +51,13 @@ async function updateSelection(sendRequest = true) {
|
|||||||
.map((s) => s.split(',')[0]));
|
.map((s) => s.split(',')[0]));
|
||||||
// drop the category if its subcategory is selected
|
// drop the category if its subcategory is selected
|
||||||
const requestTags = _.uniq(_.pullAll(selections, requestCategoryTags));
|
const requestTags = _.uniq(_.pullAll(selections, requestCategoryTags));
|
||||||
let url;
|
let q = $('[name="q"]').val().split(' ').filter((i) => !i.startsWith('category:')).join(' ');
|
||||||
if (requestTags.length === 0) {
|
if (requestTags.length) q = requestTags.map((i) => `category:${i}`).join(' ') + (q ? ` ${q}` : '');
|
||||||
url = UiContext.getProblemUrlWithoutCategory;
|
const url = new URL(window.location.href);
|
||||||
} else {
|
if (!q) url.searchParams.delete('q');
|
||||||
url = substitute(decodeURIComponent(UiContext.getProblemUrlWithCategory), {
|
else url.searchParams.set('q', q);
|
||||||
category: requestTags
|
pjax.request({ url: url.toString() });
|
||||||
.map((tag) => tag.split(',').map(encodeURIComponent).join(','))
|
$('[name="q"]').val(q);
|
||||||
.join(','), // build a beautiful URL
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const q = $('[name="q"]').val();
|
|
||||||
if (q) url += `?q=${q}`;
|
|
||||||
pjax.request({ url });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +223,7 @@ function processElement(ele) {
|
|||||||
createHint('Hint::icon::difficulty', $(ele).find('th.col--difficulty'));
|
createHint('Hint::icon::difficulty', $(ele).find('th.col--difficulty'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = new NamedPage(['problem_main', 'problem_category'], () => {
|
const page = new NamedPage(['problem_main'], () => {
|
||||||
const doc = document.body;
|
const doc = document.body;
|
||||||
doc.className += ' display-mode';
|
doc.className += ' display-mode';
|
||||||
$('.section.display-mode').removeClass('display-mode');
|
$('.section.display-mode').removeClass('display-mode');
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
{%- if pdoc.tag|length > 0 %}
|
{%- if pdoc.tag|length > 0 %}
|
||||||
{%- if not inline %}<ul class="problem__tags">{% endif -%}
|
{%- if not inline %}<ul class="problem__tags">{% endif -%}
|
||||||
{%- for tag in pdoc['tag'] %}
|
{%- for tag in pdoc['tag'] %}
|
||||||
<li class="problem__tag"><a class="problem__tag-link" href="{{ url('problem_category', category=tag) }}">{{ tag }}</a></li>
|
<li class="problem__tag"><a class="problem__tag-link" href="{{ url('problem_main', query={q:'category:'+tag}) }}">{{ tag }}</a></li>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
{%- if not inline %}</ul>{% endif -%}
|
{%- if not inline %}</ul>{% endif -%}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
{%- for category, sub_categories in model.system.get('problem.categories')|parseYaml -%}
|
{%- for category, sub_categories in model.system.get('problem.categories')|parseYaml -%}
|
||||||
<li class="group-list__item">
|
<li class="group-list__item">
|
||||||
<h2 class="section__title">
|
<h2 class="section__title">
|
||||||
<a href="{{ url('problem_category', category=category) }}" data-category="{{ category }}">{{ category }}</a>
|
<a href="{{ url('problem_main', query={q:'category:'+category}) }}" data-category="{{ category }}">{{ category }}</a>
|
||||||
</h2>
|
</h2>
|
||||||
{% if sub_categories | length > 0 %}
|
{% if sub_categories | length > 0 %}
|
||||||
<ol class="chip-list">
|
<ol class="chip-list">
|
||||||
{%- for sub_category in sub_categories -%}
|
{%- for sub_category in sub_categories -%}
|
||||||
<li class="chip-list__item">
|
<li class="chip-list__item">
|
||||||
<a class="typo-a" href="{{ url('problem_category', category=(category + ',' + sub_category)) }}">{{ sub_category }}</a>
|
<a class="typo-a" href="{{ url('problem_main', query={q:'category:'+sub_category}) }}">{{ sub_category }}</a>
|
||||||
</li>
|
</li>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</ol>
|
</ol>
|
||||||
|
|||||||
@@ -11,24 +11,6 @@
|
|||||||
<dd>{{ pdoc.config.memoryMax if pdoc.config.memoryMax == pdoc.config.memoryMin else pdoc.config.memoryMin+'~'+pdoc.config.memoryMax }}MiB</dd>
|
<dd>{{ pdoc.config.memoryMax if pdoc.config.memoryMax == pdoc.config.memoryMin else pdoc.config.memoryMin+'~'+pdoc.config.memoryMax }}MiB</dd>
|
||||||
<dt>{{ _('Difficulty') }}</dt>
|
<dt>{{ _('Difficulty') }}</dt>
|
||||||
<dd>{{ pdoc['difficulty'] or lib.difficulty(pdoc.nSubmit, pdoc.nAccept) or _('(None)') }}</dd>
|
<dd>{{ pdoc['difficulty'] or lib.difficulty(pdoc.nSubmit, pdoc.nAccept) or _('(None)') }}</dd>
|
||||||
<dt>{{ _('Category') }}</dt>
|
|
||||||
<dd>
|
|
||||||
{% if pdoc['category'] %}
|
|
||||||
<span class="hasjs--hide" name="problem-sidebar__categories">
|
|
||||||
{%- for category in pdoc['category'] -%}
|
|
||||||
<a href="{{ url('problem_category', category=category) }}">{{ category }}</a>
|
|
||||||
{% if loop.index < pdoc['category']|length %}
|
|
||||||
{{ _('problem-subcategory-delim') }}
|
|
||||||
{% endif %}
|
|
||||||
{%- endfor -%}
|
|
||||||
</span>
|
|
||||||
<span class="nojs--hide">
|
|
||||||
<a href="javascript:;" name="problem-sidebar__show-category">{{ _('page.problem_detail.sidebar.show_category') }}</a>
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
{{ _('(None)') }}
|
|
||||||
{% endif %}
|
|
||||||
</dd>
|
|
||||||
<dt>{{ _('Tags') }}</dt>
|
<dt>{{ _('Tags') }}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{{ problem.render_problem_tags(pdoc, true) }}
|
{{ problem.render_problem_tags(pdoc, true) }}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div data-fragment-id="problem_lucky">
|
<div data-fragment-id="problem_lucky">
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url('problem_random', query={category:category}) if category else url('problem_random') }}" class="expanded button" target="_blank">
|
<a href="{{ url('problem_random', query={q:qs}) if qs else url('problem_random') }}" class="expanded button" target="_blank">
|
||||||
<span class="icon icon-feeling-lucky"></span> {{ _('Lucky') }}
|
<span class="icon icon-feeling-lucky"></span> {{ _('Lucky') }}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
{% extends "layout/basic.html" %}
|
{% extends "layout/basic.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ set(UiContext, {
|
{{ set(UiContext, {
|
||||||
getProblemUrlWithCategory: url('problem_category', category='{category}'),
|
|
||||||
getProblemUrlWithoutCategory: url('problem_main'),
|
|
||||||
currentCategory: category
|
currentCategory: category
|
||||||
}) }}
|
}) }}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -13,7 +11,7 @@
|
|||||||
<div class="section__body no-padding">
|
<div class="section__body no-padding">
|
||||||
<form method="get" id="searchForm" action="{{ url('problem_main') }}">
|
<form method="get" id="searchForm" action="{{ url('problem_main') }}">
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<input name="q" type="text" value="{{ category }}" placeholder="{{ _('Search') }}">
|
<input name="q" type="text" value="{{ qs }}" placeholder="{{ _('Search') }}">
|
||||||
{% include "partials/problem_stat.html" %}
|
{% include "partials/problem_stat.html" %}
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
<span class="icon icon-search"></span>
|
<span class="icon icon-search"></span>
|
||||||
|
|||||||
Reference in new issue
Block a user