@ddj-v2/user-management 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,77 +1,72 @@
1
- # HydroOJ插件 用户管理面板
1
+ # HydroOJ插件 使用者管理面板
2
2
 
3
- 一个为 HydroOJ 提供可视化用户管理功能的插件,允许管理员在控制面板中方便地管理用户信息、权限和状态。
3
+ 修改自 https://github.com/SummerofOrange/hydrooj-user-management
4
4
 
5
- > 代码很简单,佛系不定期更新~
5
+ 這是一個為 HydroOJ 提供可視化使用者管理功能的插件,讓管理員能在控制面板中方便地管理使用者資訊、權限與狀態。
6
+
7
+ > 程式碼很簡單,佛系不定期更新~
6
8
  >
7
- > 如果认为好用请给我点一个star,不胜感激。
9
+ > 如果覺得好用請幫我點個 star,感激不盡。
8
10
 
9
- ## 安装方法
11
+ ## 安裝方法
10
12
 
11
13
  ```bash
12
14
  sudo su
13
- cd /root/.hydro/
14
- git clone https://github.com/SummerofOrange/hydrooj-user-management
15
- hydrooj addon add /root/.hydro/hydrooj-user-management
15
+ yarn global add @ddj-v2/user-management
16
+ hydrooj addon add @ddj-v2/user-management
16
17
  pm2 restart hydrooj
17
18
  ```
18
19
 
19
20
  ## 使用方法
20
21
 
21
- 1. **访问用户管理**: 登录 HydroOJ 后,在控制面板侧边栏找到"用户管理"菜单项
22
- 2. **搜索用户**: 在用户列表页面使用搜索框查找特定用户
23
- 3. **编辑用户**: 点击用户列表中的"编辑"按钮进入用户详情页面
24
- 4. **管理权限**: 在用户详情页面的"权限管理"部分设置用户权限
25
- 5. **重置密码**: 在"密码管理"部分为用户重置新密码
26
- 6. **封禁用户**: 在"用户状态"部分封禁或解封用户
22
+ 1. **進入使用者管理**:登入 HydroOJ 後,在控制面板側邊欄找到「使用者管理」選單項
23
+ 2. **搜尋使用者**:在使用者列表頁面使用搜尋框查找特定使用者
24
+ 3. **編輯使用者**:點擊使用者列表中的「編輯」按鈕進入使用者詳細頁面
25
+ 4. **管理權限**:在使用者詳細頁面的「權限管理」部分設定使用者權限
26
+ 5. **重設密碼**:在「密碼管理」部分為使用者重設新密碼
27
+ 6. **封鎖使用者**:在「使用者狀態」部分封鎖或解封使用者
28
+
27
29
 
28
- ## 权限说明
30
+ 1. **域用戶管理**: 可看見 default role
31
+ ## 權限說明
29
32
 
30
- 插件使用以下权限级别:
33
+ 插件使用以下權限等級:
31
34
 
32
- - **-1**: root(超级管理员)
33
- - **0**: 已封禁用户
34
- - **4**: 系统保留
35
- - **8**: 访客用户
36
- - **16842756**: 默认用户权限
37
- - **其他值**: 自定义权限
35
+ - **-1**:root(超級管理員)
36
+ - **0**:已封鎖使用者
37
+ - **4**:系統保留
38
+ - **8**:訪客使用者
39
+ - **16842756**:預設使用者權限
40
+ - **其他值**:自訂權限
38
41
 
39
- ## 界面展示
42
+ ## 介面展示
40
43
 
41
- ### 用户列表页面
44
+ ### 使用者列表頁面
42
45
 
43
- ![用户列表](asset/fig1.png)
46
+ ![使用者列表](asset/fig1.png)
44
47
 
45
- ### 用户详情页面
48
+ ### 使用者詳細頁面
46
49
 
47
- ![用户详情](asset/fig2.png)
50
+ ![使用者詳細](asset/fig2.png)
48
51
 
49
52
  ## 安全特性
50
53
 
51
- - ✅ 权限验证:只有具有系统管理权限的用户才能访问
52
- - ✅ 操作确认:重要操作(如重置密码、封禁用户)需要确认
53
- - ✅ 权限保护:防止非超级管理员修改超级管理员账户
54
- - ✅ 数据验证:自动验证用户名和邮箱的唯一性
55
- - ✅ 输入验证:前端和后端双重验证用户输入
54
+ - ✅ 權限驗證:只有具有系統管理權限的使用者才能訪問
55
+ - ✅ 操作確認:重要操作(如重設密碼、封鎖使用者)需確認
56
+ - ✅ 權限保護:防止非超級管理員修改超級管理員帳戶
57
+ - ✅ 資料驗證:自動驗證使用者名稱與電子郵件唯一性
58
+ - ✅ 輸入驗證:前端與後端雙重驗證使用者輸入
56
59
 
57
- ## 开发说明
60
+ ## 開發說明
58
61
 
59
- ### 贡献代码
62
+ ### 貢獻程式碼
60
63
 
61
- 欢迎提交 Issue Pull Request 来改进这个插件。
64
+ 歡迎提交 Issue Pull Request 改進此插件。
62
65
 
63
- ## 许可证
66
+ ## 授權
64
67
 
65
68
  MIT License
66
69
 
67
- ## 支持
68
-
69
- 如果您在使用过程中遇到问题,请:
70
-
71
- 1. 查看 [Issues](https://github.com/SummerofOrange/hydrooj-user-management/issues) 页面
72
- 2. 提交新的 Issue 描述您的问题
73
- 3. 联系作者获取技术支持
74
-
75
70
  ---
76
71
 
77
- **注意**: 此插件需要 HydroOJ v5.0.0-beta.6 或更高版本。使用前请确保您有足够的系统管理权限。
72
+ **注意**:此插件需 HydroOJ v5.0.0-beta.6 或更高版本。使用前請確保您有足夠的系統管理權限。
package/index.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  Context, Handler, param, PRIV, Types, UserModel, DomainModel,
3
- ValidationError, UserNotFoundError, PermissionError, Time, SystemModel, moment
3
+ ValidationError, UserNotFoundError, PermissionError, Time, SystemModel, moment,
4
+ PERM
4
5
  } from 'hydrooj';
6
+ import domain from 'hydrooj/src/model/domain';
5
7
 
6
8
  declare module 'hydrooj' {
7
9
  interface Collections {
@@ -98,11 +100,11 @@ class UserManageDetailHandler extends UserManageHandler {
98
100
  if (!udoc) throw new UserNotFoundError(uid);
99
101
 
100
102
  if (operation === 'edit') {
101
- await this.postEdit(domainId, uid);
103
+ await this.postEdit(domainId, uid, this.args.mail, this.args.uname, this.args.bio);
102
104
  } else if (operation === 'resetPassword') {
103
- await this.postResetPassword(domainId, uid);
105
+ await this.postResetPassword(domainId, uid, this.args.password);
104
106
  } else if (operation === 'setPriv') {
105
- await this.postSetPriv(domainId, uid);
107
+ await this.postSetPriv(domainId, uid, this.args.priv);
106
108
  } else if (operation === 'ban') {
107
109
  await this.postBan(domainId, uid);
108
110
  } else if (operation === 'unban') {
@@ -115,9 +117,8 @@ class UserManageDetailHandler extends UserManageHandler {
115
117
  @param('uid', Types.Int)
116
118
  @param('mail', Types.Email, true)
117
119
  @param('uname', Types.Username, true)
118
- @param('school', Types.String, true)
119
120
  @param('bio', Types.Content, true)
120
- async postEdit(domainId: string, uid: number, mail?: string, uname?: string, school?: string, bio?: string) {
121
+ async postEdit(domainId: string, uid: number, mail?: string, uname?: string, bio?: string) {
121
122
  const udoc = await UserModel.getById(domainId, uid);
122
123
  if (!udoc) throw new UserNotFoundError(uid);
123
124
 
@@ -140,7 +141,6 @@ class UserManageDetailHandler extends UserManageHandler {
140
141
  }
141
142
 
142
143
  const updates: any = {};
143
- if (school !== undefined) updates.school = school;
144
144
  if (bio !== undefined) updates.bio = bio;
145
145
 
146
146
  if (Object.keys(updates).length > 0) {
@@ -210,6 +210,77 @@ export async function apply(ctx: Context) {
210
210
  // 在控制面板侧边栏添加用户管理菜单项
211
211
  ctx.injectUI('ControlPanel', 'user_manage_main', { icon: 'user' });
212
212
 
213
+ ctx.withHandlerClass('DomainUser', (DomainUserHandler: { prototype: any }) => {
214
+ const originalGet = DomainUserHandler.prototype.get;
215
+
216
+ // 包裝原方法
217
+ DomainUserHandler.prototype.get = async function() {
218
+ const { domainId } = this.args;
219
+ const format = this.args.format || 'default';
220
+ console.log('DomainUserHandler get called with domainId:', domainId, 'format:', format);
221
+ const [dudocs, roles] = await Promise.all([
222
+ domain.collUser.aggregate([
223
+ {
224
+ $match: {
225
+ // TODO: add a page to display users who joined but with default role
226
+ role: {
227
+ $nin: ['guest'],
228
+ $ne: null,
229
+ },
230
+ domainId,
231
+ },
232
+ },
233
+ {
234
+ $lookup: {
235
+ from: 'user',
236
+ let: { uid: '$uid' },
237
+ pipeline: [
238
+ {
239
+ $match: {
240
+ $expr: { $eq: ['$_id', '$$uid'] },
241
+ priv: { $bitsAllSet: PRIV.PRIV_USER_PROFILE },
242
+ },
243
+ },
244
+ {
245
+ $project: {
246
+ _id: 1,
247
+ uname: 1,
248
+ avatar: 1,
249
+ },
250
+ },
251
+ ],
252
+ as: 'user',
253
+ },
254
+ },
255
+ { $unwind: '$user' },
256
+ {
257
+ $project: {
258
+ user: 1,
259
+ role: 1,
260
+ join: 1,
261
+ ...(this.user.hasPerm(PERM.PERM_VIEW_USER_PRIVATE_INFO) ? { displayName: 1 } : {}),
262
+ },
263
+ },
264
+ ]).toArray(),
265
+ domain.getRoles(domainId),
266
+ ]);
267
+ const users = dudocs.map((dudoc) => {
268
+ const u = {
269
+ ...dudoc,
270
+ ...dudoc.user,
271
+ };
272
+ delete u.user;
273
+ return u;
274
+ });
275
+ const rudocs: Record<string, any[]> = {};
276
+ for (const role of roles) rudocs[role._id] = users.filter((udoc) => udoc.role === role._id);
277
+ this.response.template = format === 'raw' ? 'domain_user_raw.html' : 'domain_user.html';
278
+ this.response.body = {
279
+ roles, rudocs, domain: this.domain,
280
+ };
281
+ };
282
+ return DomainUserHandler;
283
+ });
213
284
  // 添加国际化支持
214
285
  ctx.i18n.load('zh', {
215
286
  'user_manage_main': '用户管理',
@@ -275,6 +346,71 @@ export async function apply(ctx: Context) {
275
346
  'Copy User ID': '复制用户ID'
276
347
  });
277
348
 
349
+ // 添加国际化支持
350
+ ctx.i18n.load('zh_TW', {
351
+ 'user_manage_main': '用戶管理',
352
+ 'user_manage_detail': '用戶詳情',
353
+
354
+ 'User Management': '用戶管理',
355
+ 'User List': '用戶列表',
356
+ 'Search Users': '搜尋用戶',
357
+ 'Search by': '搜尋方式',
358
+ 'Username': '用戶名',
359
+ 'Email': '電子郵件',
360
+ 'User ID': '用戶ID',
361
+ 'Keyword': '關鍵字',
362
+ 'Sort by': '排序方式',
363
+ 'Registration Time': '註冊時間',
364
+ 'Last Login': '最後登入',
365
+ 'Privilege': '權限',
366
+ 'Order': '順序',
367
+ 'Ascending': '升序',
368
+ 'Descending': '降序',
369
+ 'Search': '搜尋',
370
+ 'Clear': '清除',
371
+ 'Refresh': '刷新',
372
+
373
+ 'Normal User': '普通用戶',
374
+ 'Admin': '管理員',
375
+ 'Banned': '已封禁',
376
+ 'Super Admin': '超級管理員',
377
+ 'Active': '活躍',
378
+ 'Inactive': '不活躍',
379
+ 'Actions': '操作',
380
+ 'View': '查看',
381
+ 'Edit': '編輯',
382
+ 'Ban': '封禁',
383
+ 'Unban': '解封',
384
+ 'Set Privilege': '設置權限',
385
+ 'Status': '狀態',
386
+ 'School': '學校',
387
+ 'Bio': '個人簡介',
388
+ 'Never': '從未',
389
+ 'Not set': '未設置',
390
+ 'Previous': '上一頁',
391
+ 'Next': '下一頁',
392
+ 'Page': '頁',
393
+ 'of': '共',
394
+ 'users': '用戶',
395
+ 'Total': '總計',
396
+ 'Showing': '顯示',
397
+ 'to': '到',
398
+ 'User Details': '用戶詳情',
399
+ 'Basic Information': '基本資訊',
400
+ 'User Statistics': '用戶統計',
401
+ 'Privilege Management': '權限管理',
402
+ 'Password Management': '密碼管理',
403
+ 'User Status': '用戶狀態',
404
+ 'Back to List': '返回列表',
405
+ 'Save Changes': '保存更改',
406
+ 'Cancel': '取消',
407
+ 'Reset Password': '重設密碼',
408
+ 'Current Privilege': '當前權限',
409
+ 'Ban User': '封禁用戶',
410
+ 'Unban User': '解封用戶',
411
+ 'Copy User ID': '複製用戶ID'
412
+ });
413
+
278
414
  ctx.i18n.load('en', {
279
415
  'user_manage_main': 'User Management',
280
416
  'user_manage_detail': 'User Detail',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ddj-v2/user-management",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "Advanced user management plugin for Hydro",
5
5
  "main": "index.ts",
6
6
  "author": "Grass_cat",
@@ -0,0 +1,109 @@
1
+ {% extends "domain_base.html" %}
2
+ {% block domain_content %}
3
+ {{ set(UiContext, 'roles', roles.map(eval('(i) => i._id'))) }}
4
+ {{ set(UiContext, 'canForceJoin', model.system.get('server.allowInvite') or handler.user.hasPriv(PRIV.PRIV_MANAGE_ALL_DOMAIN)) }}
5
+ {% set _rolesSelect = roles.filter(eval("(i) => i._id !== 'guest'")).map(eval('(role) => [role._id, role._id]')) %}
6
+ <div class="section">
7
+ <div class="section__header">
8
+ <h1 class="section__title">{{ _('{0}: Users').format(domain.name) }}</h1>
9
+ <div class="section__tools">
10
+ <button class="primary rounded button" name="add_user">{{ _('Add User') }}</button>
11
+ </div>
12
+ <!-- Search bar for Role -->
13
+ <div class="section__search" style="margin-top: 10px;">
14
+ <input type="text" id="roleSearch" placeholder="{{ _('Search by Role') }}" class="input" style="width: 200px;">
15
+ </div>
16
+ </div>
17
+ {{ noscript_note.render() }}
18
+ <div class="section__body no-padding domain-users">
19
+ <table class="data-table" id="userTable">
20
+ <colgroup>
21
+ <col class="col--checkbox">
22
+ <col class="col--uid">
23
+ <col class="col--user">
24
+ <col class="col--role">
25
+ </colgroup>
26
+ <thead>
27
+ <tr>
28
+ <th class="col--checkbox">
29
+ <label class="compact checkbox">
30
+ <input type="checkbox" name="select_all" data-checkbox-toggle="user">
31
+ </label>
32
+ </th>
33
+ <th class="col--uid">{{ _('User ID') }}</th>
34
+ <th class="col--user">{{ _('Username') }}</th>
35
+ <th class="col--role">{{ _('Role') }}</th>
36
+ </tr>
37
+ </thead>
38
+ <tbody>
39
+ {%- for role, udocs in rudocs -%}
40
+ {%- if udocs|length > 50 -%}
41
+ <tr data-role="{{ role }}">
42
+ <td colspan="3" style="text-wrap: wrap;"><div style="max-height: 300px; overflow-y: auto">
43
+ {%- for udoc in udocs -%}
44
+ {% set is_disabled=(rudoc._id == handler.user._id) %}
45
+ <input type="checkbox" data-uid="{{udoc._id}}" data-checkbox-group="user" {% if is_disabled %}disabled{% else %}data-checkbox-range{% endif %}>
46
+ [{{udoc._id}}]{{ user.render_inline(udoc, avatar=false, badge=false) }}{% if not udoc.join %}
47
+ <span class="not-joined">({{ _('Not joined yet') }})</span>
48
+ {% endif %}&nbsp;&nbsp;&nbsp;
49
+ {%- endfor -%}
50
+ </div></td>
51
+ <td>{{ role }}</td>
52
+ </tr>
53
+ {%- else -%}
54
+ {%- for rudoc in udocs -%}
55
+ {% set is_disabled=(rudoc._id == handler.user._id) %}
56
+ <tr data-role="{{ rudoc.role }}" data-uid="{{ rudoc._id }}">
57
+ <td class="col--checkbox">
58
+ <label class="compact checkbox">
59
+ <input type="checkbox" data-checkbox-group="user" {% if is_disabled %}disabled{% else %}data-checkbox-range{% endif %}>
60
+ </label>
61
+ </td>
62
+ <td class="col--uid">
63
+ {{ rudoc._id }}
64
+ </td>
65
+ <td class="col--user">
66
+ {{ user.render_inline(rudoc, badge=false) }}
67
+ {% if not rudoc.join %}<span class="text-orange">({{ _('Not joined yet') }})</span>{% endif %}
68
+ </td>
69
+ <td class="col--role">
70
+ {{ form.select({
71
+ options:_rolesSelect,
72
+ name:'role',
73
+ value:rudoc.role,
74
+ extra_class:'compact',
75
+ disabled:is_disabled
76
+ }) }}
77
+ </td>
78
+ </tr>
79
+ {%- endfor -%}
80
+ {%- endif -%}
81
+ {%- endfor -%}
82
+ </tbody>
83
+ </table>
84
+ </div>
85
+ <div class="section__body">
86
+ <button class="rounded button" name="remove_selected">{{ _('Remove Selected User') }}</button>
87
+ <button class="rounded button" name="set_roles">{{ _('Set Roles for Selected User') }}</button>
88
+ </div>
89
+ </div>
90
+ <script>
91
+ document.addEventListener('DOMContentLoaded', function() {
92
+ var searchInput = document.getElementById('roleSearch');
93
+ searchInput.addEventListener('input', function() {
94
+ var filter = searchInput.value.toLowerCase();
95
+ var rows = document.querySelectorAll('#userTable tbody tr');
96
+ rows.forEach(function(row) {
97
+ var role = row.getAttribute('data-role');
98
+ if (role && role.toLowerCase().indexOf(filter) !== -1) {
99
+ row.style.display = '';
100
+ } else if (filter === '') {
101
+ row.style.display = '';
102
+ } else {
103
+ row.style.display = 'none';
104
+ }
105
+ });
106
+ });
107
+ });
108
+ </script>
109
+ {% endblock %}
@@ -50,13 +50,6 @@
50
50
  </div>
51
51
  </div>
52
52
  <div class="row">
53
- <div class="medium-6 columns">
54
- {{ form.form_text({
55
- label: _('School'),
56
- name: 'school',
57
- value: udoc.school or ''
58
- }) }}
59
- </div>
60
53
  <div class="medium-6 columns">
61
54
  {{ form.form_text({
62
55
  label: _('Registration Time'),
@@ -134,14 +134,43 @@
134
134
  </div>
135
135
 
136
136
  <style>
137
- .col--uid { width: 80px; }
138
- .col--user { width: 150px; }
139
- .col--email { width: 200px; }
140
- .col--regat { width: 120px; }
141
- .col--loginat { width: 120px; }
142
- .col--priv { width: 100px; }
143
- .col--status { width: 80px; }
144
- .col--actions { width: 100px; }
137
+ .section__body.no-padding {
138
+ max-width: 100%;
139
+ overflow-x: hidden;
140
+ }
141
+
142
+ .data-table {
143
+ width: 100%;
144
+ table-layout: fixed;
145
+ }
146
+
147
+ .data-table th,
148
+ .data-table td {
149
+ word-break: break-word;
150
+ overflow-wrap: anywhere;
151
+ white-space: normal;
152
+ }
153
+
154
+ .data-table td {
155
+ max-width: 0;
156
+ }
157
+
158
+ .data-table td a,
159
+ .data-table td span,
160
+ .data-table td small {
161
+ word-break: break-word;
162
+ overflow-wrap: anywhere;
163
+ white-space: normal;
164
+ }
165
+
166
+ .col--uid { width: 8%; }
167
+ .col--user { width: 17%; }
168
+ .col--email { width: 19%; }
169
+ .col--regat { width: 13%; }
170
+ .col--loginat { width: 13%; }
171
+ .col--priv { width: 10%; }
172
+ .col--status { width: 8%; }
173
+ .col--actions { width: 12%; }
145
174
 
146
175
  .badge {
147
176
  display: inline-block;
@@ -171,6 +200,27 @@
171
200
  padding: 4px 8px;
172
201
  font-size: 12px;
173
202
  }
203
+
204
+ .col--actions .button--small {
205
+ display: block;
206
+ width: 100%;
207
+ max-width: 100%;
208
+ min-width: 0;
209
+ box-sizing: border-box;
210
+ padding: 4px 4px;
211
+ line-height: 1.2;
212
+ white-space: normal !important;
213
+ overflow-wrap: anywhere;
214
+ word-break: break-word;
215
+ text-align: center;
216
+ }
217
+
218
+ @media (max-width: 768px) {
219
+ .button--small {
220
+ padding: 3px 6px;
221
+ font-size: 11px;
222
+ }
223
+ }
174
224
  </style>
175
225
 
176
226
  <script>
package/asset/fig1.png DELETED
Binary file
package/asset/fig2.png DELETED
Binary file
@@ -1,268 +0,0 @@
1
- import $ from 'jquery';
2
- import { NamedPage } from 'vj/misc/Page';
3
- import Notification from 'vj/components/notification';
4
- import { request } from 'vj/utils';
5
- import i18n from 'vj/utils/i18n';
6
- import ActionDialog from 'vj/components/dialog/ActionDialog';
7
-
8
- const page = new NamedPage('user_manage_detail', () => {
9
- const uid = window.location.pathname.split('/').pop();
10
-
11
- // 编辑用户信息
12
- $('#edit-user-form').on('submit', async function(e) {
13
- e.preventDefault();
14
-
15
- const formData = {
16
- operation: 'updateInfo',
17
- uname: $('#uname').val().trim(),
18
- email: $('#email').val().trim(),
19
- school: $('#school').val().trim(),
20
- bio: $('#bio').val().trim()
21
- };
22
-
23
- // 验证用户名
24
- if (!formData.uname) {
25
- Notification.error(i18n('Username cannot be empty'));
26
- return;
27
- }
28
-
29
- // 验证邮箱
30
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
31
- if (!emailRegex.test(formData.email)) {
32
- Notification.error(i18n('Invalid email format'));
33
- return;
34
- }
35
-
36
- try {
37
- const response = await request.post(`/manage/users/${uid}`, formData);
38
-
39
- if (response.success) {
40
- Notification.success(i18n('User information updated successfully'));
41
- // 更新页面显示
42
- $('#display-uname').text(formData.uname);
43
- $('#display-email').text(formData.email);
44
- $('#display-school').text(formData.school || i18n('Not set'));
45
- $('#display-bio').text(formData.bio || i18n('Not set'));
46
- } else {
47
- Notification.error(response.message || i18n('Update failed'));
48
- }
49
- } catch (error) {
50
- console.error('Error:', error);
51
- Notification.error(i18n('Update failed'));
52
- }
53
- });
54
-
55
- // 重置密码
56
- $('#reset-password').on('click', async function() {
57
- const username = $('#display-uname').text();
58
-
59
- if (!confirm(i18n('Are you sure to reset password for user {0}? A new random password will be generated.', username))) {
60
- return;
61
- }
62
-
63
- try {
64
- const response = await request.post(`/manage/users/${uid}`, {
65
- operation: 'resetPassword'
66
- });
67
-
68
- if (response.success) {
69
- // 显示新密码
70
- const dialog = new ActionDialog({
71
- $body: $(`
72
- <div class="typo">
73
- <h3>${i18n('Password Reset Successfully')}</h3>
74
- <p>${i18n('New password for user {0}:', username)}</p>
75
- <div class="password-display">
76
- <input type="text" value="${response.newPassword}" readonly style="width: 100%; font-family: monospace; font-size: 14px; padding: 8px; border: 1px solid #ccc; border-radius: 4px;">
77
- </div>
78
- <p class="text-red">${i18n('Please save this password and inform the user. This password will not be shown again.')}</p>
79
- <button class="button primary" onclick="navigator.clipboard.writeText('${response.newPassword}'); $(this).text('${i18n('Copied!')}')">${i18n('Copy Password')}</button>
80
- </div>
81
- `),
82
- canCancel: false
83
- });
84
- dialog.open();
85
- } else {
86
- Notification.error(response.message || i18n('Password reset failed'));
87
- }
88
- } catch (error) {
89
- console.error('Error:', error);
90
- Notification.error(i18n('Password reset failed'));
91
- }
92
- });
93
-
94
- // 设置权限
95
- $('#set-privilege').on('click', async function() {
96
- const username = $('#display-uname').text();
97
- const currentPriv = $(this).data('current-priv');
98
-
99
- const newPriv = prompt(i18n('Enter new privilege value for user {0}:', username), currentPriv);
100
- if (newPriv === null || newPriv === '') {
101
- return;
102
- }
103
-
104
- const privValue = parseInt(newPriv, 10);
105
- if (isNaN(privValue)) {
106
- Notification.error(i18n('Invalid privilege value'));
107
- return;
108
- }
109
-
110
- if (!confirm(i18n('Are you sure to set privilege of user {0} to {1}?', username, privValue))) {
111
- return;
112
- }
113
-
114
- try {
115
- const response = await request.post(`/manage/users/${uid}`, {
116
- operation: 'setPriv',
117
- priv: privValue
118
- });
119
-
120
- if (response.success) {
121
- Notification.success(i18n('Privilege updated successfully'));
122
- // 更新页面显示
123
- $('#current-privilege').text(privValue);
124
- $(this).data('current-priv', privValue);
125
-
126
- // 更新权限徽章
127
- const $badge = $('#privilege-badge');
128
- $badge.removeClass('success warning alert');
129
- if (privValue === 0) {
130
- $badge.addClass('alert').text(i18n('Banned'));
131
- } else if (privValue === 1) {
132
- $badge.addClass('success').text(i18n('Normal User'));
133
- } else {
134
- $badge.addClass('warning').text(i18n('Admin'));
135
- }
136
- } else {
137
- Notification.error(response.message || i18n('Privilege update failed'));
138
- }
139
- } catch (error) {
140
- console.error('Error:', error);
141
- Notification.error(i18n('Privilege update failed'));
142
- }
143
- });
144
-
145
- // 封禁/解封用户
146
- $('#ban-user, #unban-user').on('click', async function() {
147
- const username = $('#display-uname').text();
148
- const action = $(this).attr('id') === 'ban-user' ? 'ban' : 'unban';
149
-
150
- const confirmMessage = action === 'ban'
151
- ? i18n('Are you sure to ban user {0}? This will set their privilege to 0.', username)
152
- : i18n('Are you sure to unban user {0}? This will restore their privilege to 1.', username);
153
-
154
- if (!confirm(confirmMessage)) {
155
- return;
156
- }
157
-
158
- try {
159
- const response = await request.post(`/manage/users/${uid}`, {
160
- operation: action
161
- });
162
-
163
- if (response.success) {
164
- Notification.success(action === 'ban' ? i18n('User banned successfully') : i18n('User unbanned successfully'));
165
-
166
- // 更新页面显示
167
- const newPriv = action === 'ban' ? 0 : 1;
168
- $('#current-privilege').text(newPriv);
169
- $('#set-privilege').data('current-priv', newPriv);
170
-
171
- // 更新权限徽章
172
- const $badge = $('#privilege-badge');
173
- $badge.removeClass('success warning alert');
174
- if (action === 'ban') {
175
- $badge.addClass('alert').text(i18n('Banned'));
176
- $('#ban-user').hide();
177
- $('#unban-user').show();
178
- } else {
179
- $badge.addClass('success').text(i18n('Normal User'));
180
- $('#ban-user').show();
181
- $('#unban-user').hide();
182
- }
183
- } else {
184
- Notification.error(response.message || i18n('Operation failed'));
185
- }
186
- } catch (error) {
187
- console.error('Error:', error);
188
- Notification.error(i18n('Operation failed'));
189
- }
190
- });
191
-
192
- // 返回用户列表
193
- $('#back-to-list').on('click', function() {
194
- window.location.href = '/manage/users';
195
- });
196
-
197
- // 刷新用户信息
198
- $('#refresh-info').on('click', function() {
199
- window.location.reload();
200
- });
201
-
202
- // 切换编辑模式
203
- $('#toggle-edit').on('click', function() {
204
- const $form = $('#edit-user-form');
205
- const $display = $('#user-info-display');
206
-
207
- if ($form.is(':visible')) {
208
- $form.hide();
209
- $display.show();
210
- $(this).text(i18n('Edit'));
211
- } else {
212
- $form.show();
213
- $display.hide();
214
- $(this).text(i18n('Cancel'));
215
- }
216
- });
217
-
218
- // 复制用户ID
219
- $('#copy-uid').on('click', function() {
220
- const uid = $(this).data('uid');
221
- navigator.clipboard.writeText(uid).then(() => {
222
- Notification.success(i18n('User ID copied to clipboard'));
223
- }).catch(() => {
224
- Notification.error(i18n('Failed to copy user ID'));
225
- });
226
- });
227
-
228
- // 键盘快捷键
229
- $(document).on('keydown', function(e) {
230
- // Esc 键取消编辑
231
- if (e.key === 'Escape' && $('#edit-user-form').is(':visible')) {
232
- $('#toggle-edit').click();
233
- }
234
-
235
- // Ctrl+S 保存编辑
236
- if (e.ctrlKey && e.key === 's' && $('#edit-user-form').is(':visible')) {
237
- e.preventDefault();
238
- $('#edit-user-form').submit();
239
- }
240
- });
241
-
242
- // 初始化提示
243
- $('[data-tooltip]').each(function() {
244
- $(this).attr('title', $(this).data('tooltip'));
245
- });
246
-
247
- // 表单验证
248
- $('#uname').on('input', function() {
249
- const value = $(this).val().trim();
250
- if (value.length < 3) {
251
- $(this).addClass('error');
252
- } else {
253
- $(this).removeClass('error');
254
- }
255
- });
256
-
257
- $('#email').on('input', function() {
258
- const value = $(this).val().trim();
259
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
260
- if (!emailRegex.test(value)) {
261
- $(this).addClass('error');
262
- } else {
263
- $(this).removeClass('error');
264
- }
265
- });
266
- });
267
-
268
- export default page;
@@ -1,200 +0,0 @@
1
- import $ from 'jquery';
2
- import { NamedPage } from 'vj/misc/Page';
3
- import Notification from 'vj/components/notification';
4
- import { request } from 'vj/utils';
5
- import i18n from 'vj/utils/i18n';
6
- import pjax from 'vj/utils/pjax';
7
-
8
- const page = new NamedPage('user_manage_main', () => {
9
- // 搜索功能
10
- $('#search-form').on('submit', function(e) {
11
- e.preventDefault();
12
- const keyword = $('#search-keyword').val().trim();
13
- const type = $('#search-type').val();
14
- const sort = $('#sort-by').val();
15
- const order = $('#sort-order').val();
16
-
17
- let url = window.location.pathname + '?';
18
- const params = [];
19
-
20
- if (keyword) {
21
- params.push(`${type}=${encodeURIComponent(keyword)}`);
22
- }
23
- if (sort) {
24
- params.push(`sort=${sort}`);
25
- }
26
- if (order) {
27
- params.push(`order=${order}`);
28
- }
29
-
30
- url += params.join('&');
31
- pjax.request({ url });
32
- });
33
-
34
- // 清空搜索
35
- $('#clear-search').on('click', function() {
36
- $('#search-keyword').val('');
37
- $('#search-type').val('uname');
38
- $('#sort-by').val('_id');
39
- $('#sort-order').val('desc');
40
- pjax.request({ url: window.location.pathname });
41
- });
42
-
43
- // 快速封禁/解封用户
44
- $('.ban-user').on('click', async function(e) {
45
- e.preventDefault();
46
- const uid = $(this).data('uid');
47
- const username = $(this).data('username');
48
- const action = $(this).data('action');
49
-
50
- const confirmMessage = action === 'ban'
51
- ? i18n('Are you sure to ban user {0}?', username)
52
- : i18n('Are you sure to unban user {0}?', username);
53
-
54
- if (!confirm(confirmMessage)) {
55
- return;
56
- }
57
-
58
- try {
59
- const response = await request.post(`/manage/users/${uid}`, {
60
- operation: action === 'ban' ? 'ban' : 'unban'
61
- });
62
-
63
- if (response.success) {
64
- Notification.success(action === 'ban' ? i18n('User banned successfully') : i18n('User unbanned successfully'));
65
- // 刷新页面
66
- window.location.reload();
67
- } else {
68
- Notification.error(response.message || i18n('Operation failed'));
69
- }
70
- } catch (error) {
71
- console.error('Error:', error);
72
- Notification.error(i18n('Operation failed'));
73
- }
74
- });
75
-
76
- // 快速设置权限
77
- $('.set-priv').on('click', async function(e) {
78
- e.preventDefault();
79
- const uid = $(this).data('uid');
80
- const username = $(this).data('username');
81
- const currentPriv = $(this).data('priv');
82
-
83
- const newPriv = prompt(i18n('Enter new privilege value for user {0}:', username), currentPriv);
84
- if (newPriv === null || newPriv === '') {
85
- return;
86
- }
87
-
88
- const privValue = parseInt(newPriv, 10);
89
- if (isNaN(privValue)) {
90
- Notification.error(i18n('Invalid privilege value'));
91
- return;
92
- }
93
-
94
- if (!confirm(i18n('Are you sure to set privilege of user {0} to {1}?', username, privValue))) {
95
- return;
96
- }
97
-
98
- try {
99
- const response = await request.post(`/manage/users/${uid}`, {
100
- operation: 'setPriv',
101
- priv: privValue
102
- });
103
-
104
- if (response.success) {
105
- Notification.success(i18n('Privilege updated successfully'));
106
- // 刷新页面
107
- window.location.reload();
108
- } else {
109
- Notification.error(response.message || i18n('Operation failed'));
110
- }
111
- } catch (error) {
112
- console.error('Error:', error);
113
- Notification.error(i18n('Operation failed'));
114
- }
115
- });
116
-
117
- // 分页链接处理
118
- $('.pagination a').on('click', function(e) {
119
- e.preventDefault();
120
- const url = $(this).attr('href');
121
- if (url && url !== '#') {
122
- pjax.request({ url });
123
- }
124
- });
125
-
126
- // 排序链接处理
127
- $('.sortable').on('click', function(e) {
128
- e.preventDefault();
129
- const sort = $(this).data('sort');
130
- const currentSort = new URLSearchParams(window.location.search).get('sort');
131
- const currentOrder = new URLSearchParams(window.location.search).get('order') || 'desc';
132
-
133
- let newOrder = 'desc';
134
- if (currentSort === sort && currentOrder === 'desc') {
135
- newOrder = 'asc';
136
- }
137
-
138
- const url = new URL(window.location);
139
- url.searchParams.set('sort', sort);
140
- url.searchParams.set('order', newOrder);
141
-
142
- pjax.request({ url: url.toString() });
143
- });
144
-
145
- // 批量操作按钮
146
- $('#batch-operations').on('click', function() {
147
- window.location.href = '/manage/users/batch';
148
- });
149
-
150
- // 导出用户数据
151
- $('#export-users').on('click', async function() {
152
- try {
153
- const response = await request.get('/manage/users', {
154
- export: 'csv'
155
- });
156
-
157
- // 创建下载链接
158
- const blob = new Blob([response], { type: 'text/csv' });
159
- const url = window.URL.createObjectURL(blob);
160
- const a = document.createElement('a');
161
- a.href = url;
162
- a.download = `users_${new Date().toISOString().split('T')[0]}.csv`;
163
- document.body.appendChild(a);
164
- a.click();
165
- document.body.removeChild(a);
166
- window.URL.revokeObjectURL(url);
167
-
168
- Notification.success(i18n('Export completed'));
169
- } catch (error) {
170
- console.error('Export error:', error);
171
- Notification.error(i18n('Export failed'));
172
- }
173
- });
174
-
175
- // 刷新按钮
176
- $('#refresh-list').on('click', function() {
177
- window.location.reload();
178
- });
179
-
180
- // 键盘快捷键
181
- $(document).on('keydown', function(e) {
182
- // Ctrl+F 聚焦搜索框
183
- if (e.ctrlKey && e.key === 'f') {
184
- e.preventDefault();
185
- $('#search-keyword').focus();
186
- }
187
-
188
- // Enter 键提交搜索
189
- if (e.key === 'Enter' && $('#search-keyword').is(':focus')) {
190
- $('#search-form').submit();
191
- }
192
- });
193
-
194
- // 初始化提示
195
- $('[data-tooltip]').each(function() {
196
- $(this).attr('title', $(this).data('tooltip'));
197
- });
198
- });
199
-
200
- export default page;