admin-resource.component.html 10 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. <header class="resource-header">
  2. <div><p>DATA RESOURCE</p><h1>{{ schema()?.label || className() }}</h1><span>{{ className() }} · {{ page()?.total || 0 | number }} 条记录</span></div>
  3. <div class="header-actions"><button type="button" (click)="load(1, true)"><lucide-icon [img]="icons.RefreshCw" [size]="17" />刷新</button>@if (schema()?.writable && schema()?.creatable !== false) { <button class="primary" type="button" (click)="openCreate()"><lucide-icon [img]="icons.Plus" [size]="18" />新建记录</button> }</div>
  4. </header>
  5. <section class="resource-card">
  6. <div class="toolbar"><label><lucide-icon [img]="icons.Search" [size]="18" /><input [ngModel]="search()" (ngModelChange)="search.set($event)" (keyup.enter)="load(1)" placeholder="按名称、标题、账号或来源键搜索" /></label><button type="button" (click)="load(1)">查询</button></div>
  7. @if (error()) { <div class="inline-error">{{ error() }}</div> }
  8. @if (loading() && !page()) {
  9. <div class="empty"><lucide-icon class="spin" [img]="icons.LoaderCircle" [size]="26" />正在加载数据…</div>
  10. } @else {
  11. <div class="table-scroll"><table><thead><tr>@for (field of columns(); track field.name) { <th>{{ field.name }}</th> }<th class="actions">操作</th></tr></thead><tbody>
  12. @for (row of page()?.results || []; track row['objectId']) {
  13. <tr>@for (field of columns(); track field.name) { <td [title]="display(row[field.name])">{{ display(row[field.name]) }}</td> }<td class="actions"><button type="button" [attr.aria-label]="schema()?.writable ? '编辑' : '查看'" (click)="openEdit(row)"><lucide-icon [img]="schema()?.writable ? icons.Pencil : icons.Eye" [size]="16" /></button>@if (schema()?.writable) { <button class="danger" type="button" aria-label="删除" (click)="remove(row)"><lucide-icon [img]="icons.Trash2" [size]="16" /></button> }</td></tr>
  14. } @empty { <tr><td class="empty-cell" [attr.colspan]="columns().length + 1">没有符合条件的数据</td></tr> }
  15. </tbody></table></div>
  16. <footer class="pagination"><span>第 {{ page()?.page || 1 }} 页,共 {{ pageCount() }} 页</span><div><button type="button" [disabled]="(page()?.page || 1) <= 1 || loading()" (click)="load((page()?.page || 1) - 1)"><lucide-icon [img]="icons.ChevronLeft" [size]="17" />上一页</button><button type="button" [disabled]="(page()?.page || 1) * (page()?.pageSize || 20) >= (page()?.total || 0) || loading()" (click)="load((page()?.page || 1) + 1)">下一页<lucide-icon [img]="icons.ChevronRight" [size]="17" /></button></div></footer>
  17. }
  18. </section>
  19. @if (editorOpen()) {
  20. <button class="drawer-mask" type="button" aria-label="关闭编辑器" (click)="closeEditor()"></button>
  21. <aside class="editor-drawer">
  22. <header><div><span>{{ !schema()?.writable ? '查看记录' : draftId() ? '编辑记录' : '新建记录' }}</span><strong>{{ schema()?.label }}</strong></div><button type="button" (click)="closeEditor()"><lucide-icon [img]="icons.X" [size]="20" /></button></header>
  23. <div class="editor-fields">
  24. @if (className() === '_User' && !draftId()) {
  25. <section class="account-create">
  26. <div class="account-create-note"><strong>开通普通用户</strong><span>自动归入当前帐套与默认组 1,并初始化旧系统用户 ID、余额和经验字段。此入口不能授予管理员权限。</span></div>
  27. <label><span>登录账号 <small>3–64 位 · 必填</small></span><input autocomplete="off" [ngModel]="createUsername()" (ngModelChange)="createUsername.set($event)" /></label>
  28. <label><span>显示名 <small>选填</small></span><input autocomplete="off" [ngModel]="createDisplayName()" (ngModelChange)="createDisplayName.set($event)" /></label>
  29. <label><span>登录密码 <small>8–72 位 · 必填</small></span><input type="password" autocomplete="new-password" [ngModel]="createPassword()" (ngModelChange)="createPassword.set($event)" /></label>
  30. <label><span>确认密码 <small>必填</small></span><input type="password" autocomplete="new-password" [ngModel]="createConfirmPassword()" (ngModelChange)="createConfirmPassword.set($event)" /></label>
  31. <label><span>手机号 <small>选填</small></span><input inputmode="tel" autocomplete="off" [ngModel]="createMobile()" (ngModelChange)="createMobile.set($event)" /></label>
  32. <label><span>邀请账号 <small>选填 · 同帐套</small></span><input autocomplete="off" [ngModel]="createInviteUsername()" (ngModelChange)="createInviteUsername.set($event)" /></label>
  33. @if (createUserError()) { <div class="account-create-error">{{ createUserError() }}</div> }
  34. </section>
  35. } @else {
  36. @for (field of schema()?.fields || []; track field.name) { @if (field.writable) {
  37. <label><span>{{ field.name }} <small>{{ field.type }}{{ field.required ? ' · 必填' : '' }}</small></span>
  38. @if (field.type === 'Boolean') { <input type="checkbox" [ngModel]="draft()[field.name] === true" (ngModelChange)="setField(field, $event)" /> }
  39. @else if (field.type === 'Object' || field.type === 'Array') { <textarea rows="5" [class.invalid]="fieldErrors()[field.name]" [ngModel]="inputValue(field)" (ngModelChange)="setField(field, $event)"></textarea>@if (fieldErrors()[field.name]; as fieldError) { <small class="field-error">{{ fieldError }}</small> } }
  40. @else { <input [type]="field.type === 'Number' ? 'number' : field.type === 'Date' ? 'datetime-local' : 'text'" [ngModel]="inputValue(field)" (ngModelChange)="setField(field, $event)" [placeholder]="field.type === 'Pointer' ? (field.targetClass + ' objectId') : field.name" /> }
  41. </label>
  42. } }
  43. @if (className() === 'CommonModel' && draftId()) {
  44. <section class="content-controls">
  45. <header><div><strong>内容审核与归档</strong><span>专用云函数同步旧 ContentManage 状态与节点移动语义</span></div><b [class.recycled]="contentStatusValue() === -2">{{ contentStatusLabel(contentStatusValue()) }}</b></header>
  46. <label><span>审核状态</span><select [ngModel]="selectedContentStatus()" (ngModelChange)="selectedContentStatus.set($event)"><option [ngValue]="99">已审核(99)</option><option [ngValue]="0">待审核(0)</option><option [ngValue]="-1">未通过(-1)</option><option [ngValue]="-3">草稿(-3)</option></select></label>
  47. <button type="button" [disabled]="updatingContent()" (click)="applyContentAction('status')">更新状态</button>
  48. <label><span>目标节点 ID</span><input type="number" min="1" [ngModel]="targetContentNodeId()" (ngModelChange)="targetContentNodeId.set(+$event)" /></label>
  49. <button type="button" [disabled]="updatingContent() || !targetContentNodeId()" (click)="applyContentAction('move')">移动节点</button>
  50. <div class="content-recycle">@if (contentStatusValue() === -2) { <button type="button" [disabled]="updatingContent()" (click)="applyContentAction('recover')">从回收站恢复</button> } @else { <button class="danger-action" type="button" [disabled]="updatingContent()" (click)="applyContentAction('recycle')">移入回收站</button> }</div>
  51. @if (contentActionError()) { <div class="content-action-status error">{{ contentActionError() }}</div> }
  52. @if (contentActionMessage()) { <div class="content-action-status success">{{ contentActionMessage() }}</div> }
  53. </section>
  54. }
  55. @if (className() === '_User' && draftId()) {
  56. <section class="user-controls">
  57. <header><div><strong>账号状态与用户组</strong><span>专用云函数会同步旧版 State / GroupID;停用账号会立即撤销会话</span></div><b [class.disabled]="draft()['isDisabled'] === true">{{ draft()['isDisabled'] === true ? '已停用' : '正常' }}</b></header>
  58. <label class="reason-field"><span>状态变更原因 <small>选填 · 最多 200 字</small></span><input [ngModel]="userActionReason()" (ngModelChange)="userActionReason.set($event)" maxlength="200" /></label>
  59. <div class="status-actions">@if (draft()['isDisabled'] === true) { <button type="button" [disabled]="updatingUser()" (click)="setUserDisabled(false)">解除停用</button> } @else { <button class="danger-action" type="button" [disabled]="updatingUser()" (click)="setUserDisabled(true)">停用并撤销会话</button> }</div>
  60. <label class="group-field"><span>用户组</span><select [ngModel]="selectedUserGroupId()" (ngModelChange)="selectedUserGroupId.set($event)"><option [ngValue]="0">请选择用户组</option>@for (group of userGroups(); track group['objectId']) { <option [ngValue]="groupIdOf(group)">{{ groupLabel(group) }}</option> }</select>@if (!userGroups().length) { <small>当前帐套尚无 Group 记录,请先在“全部数据类”中建立用户组</small> }</label>
  61. <button class="group-submit" type="button" [disabled]="updatingUser() || !selectedUserGroupId()" (click)="moveUserGroup()">保存用户组</button>
  62. @if (userActionError()) { <div class="user-action-status error">{{ userActionError() }}</div> }
  63. @if (userActionMessage()) { <div class="user-action-status success">{{ userActionMessage() }}</div> }
  64. </section>
  65. <section class="password-reset">
  66. <header><lucide-icon [img]="icons.KeyRound" [size]="18" /><div><strong>重置登录密码</strong><span>通过专用云函数写入,不进入通用资料字段</span></div></header>
  67. <label><span>新密码 <small>至少 8 位</small></span><input type="password" autocomplete="new-password" [ngModel]="newPassword()" (ngModelChange)="newPassword.set($event)" /></label>
  68. <label><span>确认密码</span><input type="password" autocomplete="new-password" [ngModel]="confirmPassword()" (ngModelChange)="confirmPassword.set($event)" /></label>
  69. @if (passwordError()) { <div class="password-status error">{{ passwordError() }}</div> }
  70. @if (passwordMessage()) { <div class="password-status success">{{ passwordMessage() }}</div> }
  71. <button type="button" [disabled]="resettingPassword()" (click)="resetPassword()">{{ resettingPassword() ? '重置中…' : '确认重置密码' }}</button>
  72. </section>
  73. }
  74. }
  75. </div>
  76. <footer><button type="button" (click)="closeEditor()">{{ schema()?.writable ? '取消' : '关闭' }}</button>@if (schema()?.writable) { <button class="primary" type="button" [disabled]="saving()" (click)="submitEditor()">{{ saving() ? '保存中…' : className() === '_User' && !draftId() ? '开通用户' : '保存记录' }}</button> }</footer>
  77. </aside>
  78. }