mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 09:46:28 +08:00
core&ui: add group bulk import/export
This commit is contained in:
5 files changed
+101
-3
No files matched your search
@@ -30,6 +30,7 @@ __langname: 简体中文
|
|||||||
'Discussion: Terms Of Service': 讨论区服务条款
|
'Discussion: Terms Of Service': 讨论区服务条款
|
||||||
'First Blood Notice\n{0} solved problem {1} ({2})': '题目一血提示\n{0} 解决了题目 {1} ({2})'
|
'First Blood Notice\n{0} solved problem {1} ({2})': '题目一血提示\n{0} 解决了题目 {1} ({2})'
|
||||||
'Format: category 1, sub category 1.1, sub category 1.2, ..., sub category 1.x, ..., category n, sub category n.1, sub category n.2, ..., sub category n.m, ...': 格式:分类1, 子分类1.1, 子分类1.2, ..., 子分类1.x, ..., 分类n, 子分类n.1, 子分类n.2, ..., 子分类n.m, ...
|
'Format: category 1, sub category 1.1, sub category 1.2, ..., sub category 1.x, ..., category n, sub category n.1, sub category n.2, ..., sub category n.m, ...': 格式:分类1, 子分类1.1, 子分类1.2, ..., 子分类1.x, ..., 分类n, 子分类n.1, 子分类n.2, ..., 子分类n.m, ...
|
||||||
|
'Format: one group per line, CSV: groupName,uid1,uid2,...': '格式:每行一个小组,CSV:组名,UID1,UID2,...'
|
||||||
'Join {0}': '加入 {0}'
|
'Join {0}': '加入 {0}'
|
||||||
'No': 否
|
'No': 否
|
||||||
'Note: Problem title may not be hidden.': 注意:题目标题可能不会被隐藏。
|
'Note: Problem title may not be hidden.': 注意:题目标题可能不会被隐藏。
|
||||||
@@ -181,6 +182,7 @@ Continue: 继续
|
|||||||
Contributed Problems: 贡献的题目
|
Contributed Problems: 贡献的题目
|
||||||
Contributions: 贡献
|
Contributions: 贡献
|
||||||
Copy Email: 复制电子邮件
|
Copy Email: 复制电子邮件
|
||||||
|
Copy the content below: 复制下方的内容
|
||||||
Copy from: 复制自
|
Copy from: 复制自
|
||||||
Copy QQ Number: 复制QQ号
|
Copy QQ Number: 复制QQ号
|
||||||
Copy WeChat Account: 复制微信号
|
Copy WeChat Account: 复制微信号
|
||||||
@@ -340,6 +342,7 @@ expand: 展开
|
|||||||
Expire: 过期
|
Expire: 过期
|
||||||
Export All Code: 导出所有代码
|
Export All Code: 导出所有代码
|
||||||
Export as {0}: 导出为 {0}
|
Export as {0}: 导出为 {0}
|
||||||
|
Export Groups: 批量导出小组
|
||||||
Extension (days): 最长延期 (日)
|
Extension (days): 最长延期 (日)
|
||||||
Extension Score Penalty: 延期递交扣分规则
|
Extension Score Penalty: 延期递交扣分规则
|
||||||
Failed to join the domain. You are already a member.: 加入域失败,您已是该域的成员。
|
Failed to join the domain. You are already a member.: 加入域失败,您已是该域的成员。
|
||||||
@@ -400,9 +403,11 @@ If left blank, the built-in template of the corresponding language will be used.
|
|||||||
Images: 图片
|
Images: 图片
|
||||||
Import From Hydro: 从 Hydro 导入
|
Import From Hydro: 从 Hydro 导入
|
||||||
Import from: 导入自
|
Import from: 导入自
|
||||||
|
Import Groups: 批量导入小组
|
||||||
Import Problem: 导入题目
|
Import Problem: 导入题目
|
||||||
Import User: 导入用户
|
Import User: 导入用户
|
||||||
Import: 导入
|
Import: 导入
|
||||||
|
Imported successfully.: 导入成功。
|
||||||
In 1 day: 一天后
|
In 1 day: 一天后
|
||||||
In 1 month: 一个月后
|
In 1 month: 一个月后
|
||||||
In 1 week: 一周后
|
In 1 week: 一周后
|
||||||
@@ -549,6 +554,7 @@ Open Source: 开源
|
|||||||
Open: 开放
|
Open: 开放
|
||||||
Operating System: 操作系统
|
Operating System: 操作系统
|
||||||
Ops: 运维
|
Ops: 运维
|
||||||
|
Or paste CSV content: 或粘贴 CSV 内容
|
||||||
Or, with automatically filled invitation code: 或者,这是可以自动填写邀请码的
|
Or, with automatically filled invitation code: 或者,这是可以自动填写邀请码的
|
||||||
Ordered List: 有序列表
|
Ordered List: 有序列表
|
||||||
Original Score: 原始分数
|
Original Score: 原始分数
|
||||||
@@ -850,6 +856,7 @@ Unstar: 取消收藏
|
|||||||
Update Permission: 更新权限
|
Update Permission: 更新权限
|
||||||
Update Settings: 更新设置
|
Update Settings: 更新设置
|
||||||
Update: 更新
|
Update: 更新
|
||||||
|
Upload CSV File: 上传 CSV 文件
|
||||||
Upload File: 上传文件
|
Upload File: 上传文件
|
||||||
Upload Problem: 上传题目
|
Upload Problem: 上传题目
|
||||||
Upload: 上传
|
Upload: 上传
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ __langname: 正體中文
|
|||||||
'Discussion: Terms Of Service': '討論區服務條款'
|
'Discussion: Terms Of Service': '討論區服務條款'
|
||||||
'First Blood Notice\n{0} solved problem {1} ({2})': '題目首殺提示\n{0} 解決了題目 {1} ({2})'
|
'First Blood Notice\n{0} solved problem {1} ({2})': '題目首殺提示\n{0} 解決了題目 {1} ({2})'
|
||||||
'Format: category 1, sub category 1.1, sub category 1.2, ..., sub category 1.x, ..., category n, sub category n.1, sub category n.2, ..., sub category n.m, ...': '格式:分類1, 子分類1.1, 子分類1.2, ..., 子分類1.x, ..., 分類n, 子分類n.1, 子分類n.2, ..., 子分類n.m, ...'
|
'Format: category 1, sub category 1.1, sub category 1.2, ..., sub category 1.x, ..., category n, sub category n.1, sub category n.2, ..., sub category n.m, ...': '格式:分類1, 子分類1.1, 子分類1.2, ..., 子分類1.x, ..., 分類n, 子分類n.1, 子分類n.2, ..., 子分類n.m, ...'
|
||||||
|
'Format: one group per line, CSV: groupName,uid1,uid2,...': '格式:每行一個小組,CSV:組名,UID1,UID2,...'
|
||||||
'No': '否'
|
'No': '否'
|
||||||
'Note: Problem title may not be hidden.': '注意:題目標題可能不會被隱藏。'
|
'Note: Problem title may not be hidden.': '注意:題目標題可能不會被隱藏。'
|
||||||
'Or upload a file:': '或者上傳一個檔案:'
|
'Or upload a file:': '或者上傳一個檔案:'
|
||||||
@@ -177,6 +178,7 @@ Continue: 繼續
|
|||||||
Contributed Problems: 貢獻的題目
|
Contributed Problems: 貢獻的題目
|
||||||
Contributions: 貢獻
|
Contributions: 貢獻
|
||||||
Copy Email: 複製電子郵件
|
Copy Email: 複製電子郵件
|
||||||
|
Copy the content below: 複製下方的內容
|
||||||
Copy from: 複製自
|
Copy from: 複製自
|
||||||
Copy QQ Number: 複製QQ號
|
Copy QQ Number: 複製QQ號
|
||||||
Copy WeChat Account: 複製微訊號
|
Copy WeChat Account: 複製微訊號
|
||||||
@@ -337,6 +339,7 @@ expand: 展開
|
|||||||
Expire: 過期
|
Expire: 過期
|
||||||
Export All Code: 匯出所有原始碼
|
Export All Code: 匯出所有原始碼
|
||||||
Export as {0}: 匯出為 {0}
|
Export as {0}: 匯出為 {0}
|
||||||
|
Export Groups: 批量匯出小組
|
||||||
Extension (days): 最長延期(日)
|
Extension (days): 最長延期(日)
|
||||||
Extension Score Penalty: 延期遞交扣分規則
|
Extension Score Penalty: 延期遞交扣分規則
|
||||||
Failed to join the domain. You are already a member.: 加入域失敗,您已是該域的成員。
|
Failed to join the domain. You are already a member.: 加入域失敗,您已是該域的成員。
|
||||||
@@ -397,9 +400,11 @@ If left blank, the built-in template of the corresponding language will be used.
|
|||||||
Images: 圖片
|
Images: 圖片
|
||||||
Import From Hydro: 從 Hydro 匯入
|
Import From Hydro: 從 Hydro 匯入
|
||||||
Import from: 匯入自
|
Import from: 匯入自
|
||||||
|
Import Groups: 批量匯入小組
|
||||||
Import Problem: 匯入題目
|
Import Problem: 匯入題目
|
||||||
Import User: 匯入使用者
|
Import User: 匯入使用者
|
||||||
Import: 匯入
|
Import: 匯入
|
||||||
|
Imported successfully.: 匯入成功。
|
||||||
In 1 day: 一天後
|
In 1 day: 一天後
|
||||||
In 1 month: 一個月後
|
In 1 month: 一個月後
|
||||||
In 1 week: 一週後
|
In 1 week: 一週後
|
||||||
@@ -547,6 +552,7 @@ Open Source: 開源
|
|||||||
Open: 開放
|
Open: 開放
|
||||||
Operating System: 作業系統
|
Operating System: 作業系統
|
||||||
Ops: 運維
|
Ops: 運維
|
||||||
|
Or paste CSV content: 或貼上 CSV 內容
|
||||||
Or, with automatically filled invitation code: 或者,可以使用自動填寫邀請碼
|
Or, with automatically filled invitation code: 或者,可以使用自動填寫邀請碼
|
||||||
Ordered List: 有序列表
|
Ordered List: 有序列表
|
||||||
Original Score: 原始得分
|
Original Score: 原始得分
|
||||||
@@ -849,6 +855,7 @@ Unstar: 取消收藏
|
|||||||
Update Permission: 更新許可權
|
Update Permission: 更新許可權
|
||||||
Update Settings: 更新設定
|
Update Settings: 更新設定
|
||||||
Update: 更新
|
Update: 更新
|
||||||
|
Upload CSV File: 上傳 CSV 檔案
|
||||||
Upload File: 上傳檔案
|
Upload File: 上傳檔案
|
||||||
Upload Problem: 上傳題目
|
Upload Problem: 上傳題目
|
||||||
Upload: 上傳
|
Upload: 上傳
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export class ConfirmDialog extends Dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Field {
|
export interface Field {
|
||||||
type: 'text' | 'checkbox' | 'user' | 'userId' | 'username' | 'domain';
|
type: 'text' | 'textarea' | 'checkbox' | 'user' | 'userId' | 'username' | 'domain';
|
||||||
options?: string[] | Record<string, string>;
|
options?: string[] | Record<string, string>;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -122,10 +122,11 @@ export interface Field {
|
|||||||
required?: boolean;
|
required?: boolean;
|
||||||
default?: string;
|
default?: string;
|
||||||
columns?: number;
|
columns?: number;
|
||||||
|
rows?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Result<T extends string, R extends Record<T, Field>> = {
|
type Result<T extends string, R extends Record<T, Field>> = {
|
||||||
[K in keyof R]: R[K]['type'] extends ('text' | 'password' | 'username' | 'domain') ? string
|
[K in keyof R]: R[K]['type'] extends ('text' | 'password' | 'username' | 'domain' | 'textarea') ? string
|
||||||
: R[K]['type'] extends 'checkbox' ? boolean
|
: R[K]['type'] extends 'checkbox' ? boolean
|
||||||
: R[K]['type'] extends 'userId' ? number
|
: R[K]['type'] extends 'userId' ? number
|
||||||
: R[K]['type'] extends 'user' ? any
|
: R[K]['type'] extends 'user' ? any
|
||||||
@@ -175,6 +176,18 @@ export async function prompt<T extends string, R extends Record<T, Field>>(title
|
|||||||
</div></div>
|
</div></div>
|
||||||
{layout.map((i) => <div className="row" key={i[0][0]}>
|
{layout.map((i) => <div className="row" key={i[0][0]}>
|
||||||
{i.map(([name, field]: [string, Field]) => <div key={name} className={`columns medium-${Math.abs(field.columns || 12)}`}>
|
{i.map(([name, field]: [string, Field]) => <div key={name} className={`columns medium-${Math.abs(field.columns || 12)}`}>
|
||||||
|
{field.type === 'textarea' && <label>
|
||||||
|
{field.label}
|
||||||
|
<textarea
|
||||||
|
className="textbox"
|
||||||
|
rows={field.rows || 6}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
defaultValue={field.default}
|
||||||
|
data-autofocus={field.autofocus}
|
||||||
|
style={{ fontFamily: 'monospace' }}
|
||||||
|
onChange={(e) => setValues({ ...values, [name]: e.target.value })}
|
||||||
|
/>
|
||||||
|
</label>}
|
||||||
{['text', 'user', 'userId', 'username', 'domain'].includes(field.type) && <label>
|
{['text', 'user', 'userId', 'username', 'domain'].includes(field.type) && <label>
|
||||||
{field.label}
|
{field.label}
|
||||||
<div className="textbox-container">
|
<div className="textbox-container">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
|
import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
|
||||||
import { ActionDialog, ConfirmDialog } from 'vj/components/dialog';
|
import { ActionDialog, ConfirmDialog, prompt } from 'vj/components/dialog';
|
||||||
import Notification from 'vj/components/notification';
|
import Notification from 'vj/components/notification';
|
||||||
import { NamedPage } from 'vj/misc/Page';
|
import { NamedPage } from 'vj/misc/Page';
|
||||||
import {
|
import {
|
||||||
@@ -132,7 +132,76 @@ const page = new NamedPage('domain_group', () => {
|
|||||||
Notification.success(i18n('Saved.'));
|
Notification.success(i18n('Saved.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleClickImportGroups() {
|
||||||
|
const result = await prompt(i18n('Import Groups'), {
|
||||||
|
groups: {
|
||||||
|
type: 'textarea',
|
||||||
|
label: i18n('Format: one group per line, CSV: groupName,uid1,uid2,...'),
|
||||||
|
rows: 10,
|
||||||
|
required: true,
|
||||||
|
placeholder: 'group1,1001,1002,1003\ngroup2,1004,1005',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!result) return;
|
||||||
|
const lines = String(result.groups || '').replace(/^\uFEFF/, '').split('\n');
|
||||||
|
const parsed = new Map<string, number[]>();
|
||||||
|
const errors: string[] = [];
|
||||||
|
lines.forEach((raw, idx) => {
|
||||||
|
const line = raw.trim();
|
||||||
|
if (!line) return;
|
||||||
|
const [name, ...rest] = line.split(',').map((t) => t.trim());
|
||||||
|
if (!name) return;
|
||||||
|
const uids: number[] = [];
|
||||||
|
for (const t of rest) {
|
||||||
|
if (!t) continue;
|
||||||
|
const uid = +t;
|
||||||
|
if (!Number.isInteger(uid)) {
|
||||||
|
errors.push(i18n('Line {0}: Invalid UID "{1}".', idx + 1, t));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uids.push(uid);
|
||||||
|
}
|
||||||
|
parsed.set(name, Array.from(new Set(uids)));
|
||||||
|
});
|
||||||
|
if (errors.length) {
|
||||||
|
Notification.error(errors.join('\n'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!parsed.size) {
|
||||||
|
Notification.error(i18n('No groups to import.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
for (const [name, uids] of parsed) {
|
||||||
|
await update(name, uids);
|
||||||
|
}
|
||||||
|
Notification.success(i18n('Imported successfully.'));
|
||||||
|
await delay(1500);
|
||||||
|
window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
Notification.error(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleClickExportGroups() {
|
||||||
|
const rows: string[] = [];
|
||||||
|
for (const gid of Object.keys(targets)) {
|
||||||
|
const uids = targets[gid].value() as number[];
|
||||||
|
rows.push([gid, ...uids].join(','));
|
||||||
|
}
|
||||||
|
await prompt(i18n('Export Groups'), {
|
||||||
|
groups: {
|
||||||
|
type: 'textarea',
|
||||||
|
label: i18n('Copy the content below'),
|
||||||
|
rows: 15,
|
||||||
|
default: rows.join('\n'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$('[name="create_group"]').click(() => handleClickCreateGroup());
|
$('[name="create_group"]').click(() => handleClickCreateGroup());
|
||||||
|
$('[name="import_groups"]').click(() => handleClickImportGroups());
|
||||||
|
$('[name="export_groups"]').click(() => handleClickExportGroups());
|
||||||
$('[name="remove_selected"]').click(() => handleClickDeleteSelected());
|
$('[name="remove_selected"]').click(() => handleClickDeleteSelected());
|
||||||
$('[name="save_all"]').on('click', () => handleClickSaveAll());
|
$('[name="save_all"]').on('click', () => handleClickSaveAll());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
<div class="section__header">
|
<div class="section__header">
|
||||||
<h1 class="section__title">{{ _('{0}: Groups').format(domain.name) }}</h1>
|
<h1 class="section__title">{{ _('{0}: Groups').format(domain.name) }}</h1>
|
||||||
<div class="section__tools">
|
<div class="section__tools">
|
||||||
|
<button class="rounded button" name="import_groups">{{ _('Import Groups') }}</button>
|
||||||
|
<button class="rounded button" name="export_groups">{{ _('Export Groups') }}</button>
|
||||||
<button class="primary rounded button" name="create_group">{{ _('Create Group') }}</button>
|
<button class="primary rounded button" name="create_group">{{ _('Create Group') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user