core&ui: problem_main: support sort

This commit is contained in:
undefined committed 2025-10-26 23:24:39 +08:00
1 parent e8573d7061
commit 2e9e93c9e1
10 files changed
+45 -15

No files matched your search

+1 -1
View File
@@ -310,7 +310,7 @@ class HomeSecurityHandler extends Handler {
userName: `${this.user.uname}(${this.user.mail})`, userName: `${this.user.uname}(${this.user.mail})`,
attestationType: 'direct', attestationType: 'direct',
excludeCredentials: this.user._authenticators.map((c) => ({ excludeCredentials: this.user._authenticators.map((c) => ({
id: isoBase64URL.fromBuffer(c.credentialID.buffer), id: isoBase64URL.fromBuffer(new Uint8Array(c.credentialID.buffer)),
type: 'public-key', type: 'public-key',
})), })),
authenticatorSelection: { authenticatorSelection: {
+10 -3
View File
@@ -105,10 +105,12 @@ export class ProblemMainHandler extends Handler {
@param('limit', Types.PositiveInt, true) @param('limit', Types.PositiveInt, true)
@param('pjax', Types.Boolean) @param('pjax', Types.Boolean)
@param('quick', Types.Boolean) @param('quick', Types.Boolean)
async get(domainId: string, page = 1, q = '', limit: number, pjax = false, quick = false) { @param('sort', Types.Range(['default', 'recent']), true)
async get(domainId: string, page = 1, q = '', limit: number, pjax = false, quick = false, sortStrategy = 'default') {
this.response.template = 'problem_main.html'; this.response.template = 'problem_main.html';
if (!limit || limit > this.ctx.setting.get('pagination.problem') || page > 1) limit = this.ctx.setting.get('pagination.problem'); if (!limit || limit > this.ctx.setting.get('pagination.problem') || page > 1) limit = this.ctx.setting.get('pagination.problem');
this.queryContext.query = buildQuery(this.user); this.queryContext.query = buildQuery(this.user);
if (sortStrategy === 'recent') this.queryContext.hint = 'basic';
// eslint-disable-next-line ts/no-shadow // eslint-disable-next-line ts/no-shadow
const query = this.queryContext.query; const query = this.queryContext.query;
const psdict = {}; const psdict = {};
@@ -145,11 +147,15 @@ export class ProblemMainHandler extends Handler {
} }
const sort = this.queryContext.sort; const sort = this.queryContext.sort;
await this.ctx.parallel('problem/list', query, this, sort); await this.ctx.parallel('problem/list', query, this, sort);
const sortKey = ({
default: { sort: 1, docId: 1 },
recent: { docId: -1 },
} as const)[sortStrategy];
let [pdocs, ppcount, pcount] = this.queryContext.fail let [pdocs, ppcount, pcount] = this.queryContext.fail
? [[], 0, 0] ? [[], 0, 0]
: await this.paginate( : await this.paginate(
problem.getMulti(domainId, query, quick ? ['title', 'pid', 'domainId', 'docId'] : undefined) problem.getMulti(domainId, query, quick ? ['title', 'pid', 'domainId', 'docId'] : undefined)
.sort({ sort: 1, docId: 1 }).hint(this.queryContext.hint), .sort(sortKey).hint(this.queryContext.hint),
sort.length ? 1 : page, limit, sort.length ? 1 : page, limit,
); );
if (total) { if (total) {
@@ -169,7 +175,7 @@ export class ProblemMainHandler extends Handler {
title: this.renderTitle(this.translate('problem_main')), title: this.renderTitle(this.translate('problem_main')),
fragments: (await Promise.all([ fragments: (await Promise.all([
this.renderHTML('partials/problem_list.html', { this.renderHTML('partials/problem_list.html', {
page, ppcount, pcount, pdocs, psdict, qs: q, page, ppcount, pcount, pdocs, psdict, qs: q, sort: sortStrategy,
}), }),
this.renderHTML('partials/problem_stat.html', { pcount, pcountRelation: this.queryContext.pcountRelation }), this.renderHTML('partials/problem_stat.html', { pcount, pcountRelation: this.queryContext.pcountRelation }),
this.renderHTML('partials/problem_lucky.html', { qs: q }), this.renderHTML('partials/problem_lucky.html', { qs: q }),
@@ -184,6 +190,7 @@ export class ProblemMainHandler extends Handler {
pdocs, pdocs,
psdict, psdict,
qs: q, qs: q,
sort: sortStrategy,
}; };
} }
} }
+3 -3
View File
@@ -158,7 +158,7 @@ class UserWebauthnHandler extends Handler {
if (!udoc._id) throw new UserNotFoundError(uname || 'user'); if (!udoc._id) throw new UserNotFoundError(uname || 'user');
if (!udoc.authn) throw new AuthOperationError('authn', 'disabled'); if (!udoc.authn) throw new AuthOperationError('authn', 'disabled');
allowCredentials = udoc._authenticators.map((authenticator) => ({ allowCredentials = udoc._authenticators.map((authenticator) => ({
id: isoBase64URL.fromBuffer(authenticator.credentialID.buffer), id: isoBase64URL.fromBuffer(new Uint8Array(authenticator.credentialID.buffer)),
})); }));
uid = udoc._id; uid = udoc._id;
} }
@@ -194,8 +194,8 @@ class UserWebauthnHandler extends Handler {
expectedRPID: this.getAuthnHost(), expectedRPID: this.getAuthnHost(),
credential: { credential: {
...authenticator, ...authenticator,
id: isoBase64URL.fromBuffer(authenticator.credentialID.buffer), id: isoBase64URL.fromBuffer(new Uint8Array(authenticator.credentialID.buffer)),
publicKey: authenticator.credentialPublicKey.buffer, publicKey: new Uint8Array(authenticator.credentialPublicKey.buffer),
}, },
}).catch(() => null); }).catch(() => null);
if (!verification?.verified) throw new ValidationError('authenticator'); if (!verification?.verified) throw new ValidationError('authenticator');
+2 -2
View File
@@ -414,9 +414,9 @@ export async function apply(ctx: Context) {
coll.deleteMany({ domainId }), coll.deleteMany({ domainId }),
collStatus.deleteMany({ domainId }), collStatus.deleteMany({ domainId }),
])); ]));
await db.clearIndexes(coll, ['tag', 'hidden']); await ctx.db.clearIndexes(coll, ['tag', 'hidden']);
const onlyFor = (docType: number) => ({ partialFilterExpression: { docType } }); const onlyFor = (docType: number) => ({ partialFilterExpression: { docType } });
await db.ensureIndexes( await ctx.db.ensureIndexes(
coll, coll,
{ key: { domainId: 1, docType: 1, docId: 1 }, name: 'basic', unique: true }, { key: { domainId: 1, docType: 1, docId: 1 }, name: 'basic', unique: true },
{ key: { domainId: 1, docType: 1, owner: 1, docId: -1 }, name: 'owner' }, { key: { domainId: 1, docType: 1, owner: 1, docId: -1 }, name: 'owner' },
@@ -9,7 +9,7 @@ const ProblemSelectAutoComplete = forwardRef<AutoCompleteHandle<ProblemDoc>, Aut
ref={ref as any} ref={ref as any}
cacheKey={`problem-${UiContext.domainId}`} cacheKey={`problem-${UiContext.domainId}`}
queryItems={async (query) => { queryItems={async (query) => {
const { pdocs } = await request.get(`/d/${UiContext.domainId}/p`, { q: query, quick: true }); const { pdocs } = await request.get(`/d/${UiContext.domainId}/p`, { q: query, quick: true, sort: query ? 'default' : 'recent' });
return pdocs; return pdocs;
}} }}
fetchItems={(ids) => api('problems', { ids: ids.map((i) => +i) }, ['docId', 'pid', 'title'])} fetchItems={(ids) => api('problems', { ids: ids.map((i) => +i) }, ['docId', 'pid', 'title'])}
@@ -29,7 +29,7 @@ const ProblemSelectAutoComplete = forwardRef<AutoCompleteHandle<ProblemDoc>, Aut
listStyle: {}, listStyle: {},
multi: false, multi: false,
selectedKeys: [], selectedKeys: [],
allowEmptyQuery: false, allowEmptyQuery: true,
freeSolo: false, freeSolo: false,
freeSoloConverter: (input) => input, freeSoloConverter: (input) => input,
...props, ...props,
+3 -1
View File
@@ -27,9 +27,9 @@ problem_import: Import Problem
problem_main: Problem Set problem_main: Problem Set
problem_solution: Problem Solution problem_solution: Problem Solution
problem_submit: Problem Submit problem_submit: Problem Submit
problem_type.communication: 'Type: Communication'
problem_type.default: 'Type: Default' problem_type.default: 'Type: Default'
problem_type.interactive: 'Type: Interactive' problem_type.interactive: 'Type: Interactive'
problem_type.communication: 'Type: Communication'
problem_type.objective: 'Type: Objective' problem_type.objective: 'Type: Objective'
problem_type.remote_judge: 'Type: RemoteJudge' problem_type.remote_judge: 'Type: RemoteJudge'
problem_type.submit_answer: 'Type: SubmitAnswer' problem_type.submit_answer: 'Type: SubmitAnswer'
@@ -54,6 +54,8 @@ setting_info: Personal Info
setting_preference: Preference setting_preference: Preference
setting_privacy: Privacy setting_privacy: Privacy
setting_usage: Usage Preference setting_usage: Usage Preference
sort::default: Default
sort::recent: Recent
tasks_list: Task list tasks_list: Task list
timeago_locale: en_US timeago_locale: en_US
training_create: Training Create training_create: Training Create
+2
View File
@@ -877,6 +877,8 @@ Sorry, there are no training plans.: 目前没有训练计划。
Sorry, there are no users in this domain.: 对不起,此域中没有用户。 Sorry, there are no users in this domain.: 对不起,此域中没有用户。
Sorry: 对不起 Sorry: 对不起
Sort by: 排序 Sort by: 排序
sort::default: 默认排序
sort::recent: 最新题目
Source type: 来源类型 Source type: 来源类型
Source: 来源 Source: 来源
Standard mode: 标准模式 Standard mode: 标准模式
@@ -204,7 +204,7 @@
color: #000000 color: #000000
.search > button .search > button
width: 40px width: 37px
height: 36px height: 36px
border: 1px solid #00B4CC border: 1px solid #00B4CC
background: #00B4CC background: #00B4CC
@@ -215,6 +215,16 @@
.search > div .search > div
position: absolute position: absolute
right: 50px right: 170px
top: 8px top: 10px
font-size: rem(17px) font-size: rem(17px)
.search-sort
position: absolute
border: none
top: 1px
height: 34px
right: 37px
color: #000000
cursor: pointer
width: 120px
@@ -85,9 +85,12 @@ function updateSelection() {
function loadQuery() { function loadQuery() {
const q = $('[name="q"]').val().toString(); const q = $('[name="q"]').val().toString();
const sort = $('[name="sort"]').val().toString();
const url = new URL(window.location.href); const url = new URL(window.location.href);
if (!q) url.searchParams.delete('q'); if (!q) url.searchParams.delete('q');
else url.searchParams.set('q', q); else url.searchParams.set('q', q);
if (sort && sort !== 'default') url.searchParams.set('sort', sort);
else url.searchParams.delete('sort');
url.searchParams.delete('page'); url.searchParams.delete('page');
pjax.request({ url: url.toString() }); pjax.request({ url: url.toString() });
} }
@@ -443,6 +446,7 @@ const page = new NamedPage(['problem_main'], () => {
}); });
$('#searchForm').on('submit', inputChanged); $('#searchForm').on('submit', inputChanged);
$('#searchForm').find('input').on('input', _.debounce(inputChanged, 500)); $('#searchForm').find('input').on('input', _.debounce(inputChanged, 500));
$('#searchForm').find('select[name="sort"]').on('change', inputChanged);
$('.dialog-button').on('click', (ev) => { $('.dialog-button').on('click', (ev) => {
categoryDialog.clear().open(); categoryDialog.clear().open();
ev.preventDefault(); ev.preventDefault();
@@ -10,6 +10,11 @@
<div class="search"> <div class="search">
<input name="q" type="text" value="{{ qs }}" placeholder="{{ _('Search') }}"> <input name="q" type="text" value="{{ qs }}" placeholder="{{ _('Search') }}">
{% include "partials/problem_stat.html" %} {% include "partials/problem_stat.html" %}
<select name="sort" class="search-sort">
{%- for sortType in ["default", "recent"] -%}
<option value="{{ sortType }}" {{ "selected" if sort == sortType else "" }}>{{ _('sort::' + sortType) }}</option>
{%- endfor -%}
</select>
<button type="submit"> <button type="submit">
<span class="icon icon-search"></span> <span class="icon icon-search"></span>
</button> </button>