admin-resource.component.html 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  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 (className() === 'Grade') {
  8. <div class="bulk-toolbar"><strong>多级字典范围</strong><select [ngModel]="filterGradeCategoryId()" (ngModelChange)="filterGradeCategoryId.set(+$event); filterGradeParentId.set(0); load(1)"><option [ngValue]="0">请选择分类</option>@for (category of gradeCategories(); track category['objectId']) { <option [ngValue]="gradeCategoryIdOf(category)">{{ category['cateName'] }}({{ category['cateId'] }})</option> }</select><input class="bulk-node" type="number" min="0" [ngModel]="filterGradeParentId()" (ngModelChange)="filterGradeParentId.set(+$event)" placeholder="父选项 ID" /><button type="button" [disabled]="!filterGradeCategoryId()" (click)="load(1)">查看该层</button></div>
  9. }
  10. @if (className() === 'Search') {
  11. <div class="bulk-toolbar"><strong>快捷入口范围</strong><select [ngModel]="filterSearchNavigationType()" (ngModelChange)="filterSearchNavigationType.set(+$event); load(1)"><option [ngValue]="1">管理后台</option><option [ngValue]="2">用户中心</option></select><select [ngModel]="filterSearchNavigationState()" (ngModelChange)="filterSearchNavigationState.set(+$event); load(1)"><option [ngValue]="-100">全部状态</option><option [ngValue]="1">已启用</option><option [ngValue]="2">已停用</option></select><select [ngModel]="filterSearchNavigationElite()" (ngModelChange)="filterSearchNavigationElite.set(+$event); load(1)"><option [ngValue]="-100">全部推荐</option><option [ngValue]="1">已推荐</option><option [ngValue]="0">未推荐</option></select></div>
  12. }
  13. @if (className() === 'AdZone') {
  14. <div class="bulk-toolbar"><strong>广告位范围</strong><input [ngModel]="filterAdZoneType()" (ngModelChange)="filterAdZoneType.set($event)" (keyup.enter)="load(1)" placeholder="广告位类型(精确匹配)" /><select [ngModel]="filterAdStatus()" (ngModelChange)="filterAdStatus.set(+$event); load(1)"><option [ngValue]="-100">全部状态</option><option [ngValue]="99">已启用</option><option [ngValue]="0">已停用</option></select><button type="button" (click)="load(1)">筛选</button></div>
  15. }
  16. @if (className() === 'AdInfo') {
  17. <div class="bulk-toolbar"><strong>广告内容范围</strong><select [ngModel]="filterAdInfoZoneId()" (ngModelChange)="filterAdInfoZoneId.set($event); load(1)"><option value="">全部广告位</option>@for (zone of adZones(); track zone['objectId']) { <option [value]="adZoneIdOf(zone)">{{ adZoneLabel(zone) }}</option> }</select><select [ngModel]="filterAdStatus()" (ngModelChange)="filterAdStatus.set(+$event); load(1)"><option [ngValue]="-100">全部状态</option><option [ngValue]="99">已审核</option><option [ngValue]="0">未审核</option></select></div>
  18. }
  19. @if (className() === 'FontPicShape') {
  20. <div class="bulk-toolbar"><strong>图形素材范围</strong><select [ngModel]="filterFontShapeTypeId()" (ngModelChange)="filterFontShapeTypeId.set(+$event); load(1)"><option [ngValue]="0">全部分类</option>@for (type of fontShapeTypes(); track type['objectId']) { <option [ngValue]="fontShapeTypeIdOf(type)">{{ fontShapeTypeLabel(type) }}</option> }</select></div>
  21. }
  22. @if (bulkEnabled() && selectedIds().length) {
  23. <div class="bulk-toolbar"><strong>已选 {{ selectedIds().length }} 条</strong>
  24. @if (className() === '_User') {
  25. <input class="bulk-reason" maxlength="200" [ngModel]="userActionReason()" (ngModelChange)="userActionReason.set($event)" placeholder="状态原因(选填)" />
  26. <button class="danger-action" type="button" [disabled]="updatingUser()" (click)="applyBulkUserAction('lock')">停用</button><button type="button" [disabled]="updatingUser()" (click)="applyBulkUserAction('unlock')">解锁</button>
  27. <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><button type="button" [disabled]="updatingUser() || !selectedUserGroupId()" (click)="applyBulkUserAction('move')">移动分组</button>
  28. } @else if (className() === 'CommonModel') {
  29. <select [ngModel]="selectedContentStatus()" (ngModelChange)="selectedContentStatus.set($event)"><option [ngValue]="99">已审核</option><option [ngValue]="0">待审核</option><option [ngValue]="-1">未通过</option><option [ngValue]="-3">草稿</option></select><button type="button" [disabled]="updatingContent()" (click)="applyBulkContentAction('status')">更新状态</button><button class="danger-action" type="button" [disabled]="updatingContent()" (click)="applyBulkContentAction('recycle')">回收</button><button type="button" [disabled]="updatingContent()" (click)="applyBulkContentAction('recover')">恢复</button>
  30. <input class="bulk-node" type="number" min="1" [ngModel]="targetContentNodeId()" (ngModelChange)="targetContentNodeId.set(+$event)" placeholder="节点 ID" /><button type="button" [disabled]="updatingContent() || !targetContentNodeId()" (click)="applyBulkContentAction('move')">移动节点</button>
  31. } @else if (className() === 'Node') {
  32. <button class="danger-action" type="button" [disabled]="updatingNode()" (click)="applyBulkNodeAction('recycle')">回收</button><button type="button" [disabled]="updatingNode()" (click)="applyBulkNodeAction('recover')">恢复</button>
  33. <input class="bulk-node" type="number" min="0" [ngModel]="targetNodeParentId()" (ngModelChange)="targetNodeParentId.set(+$event)" placeholder="父节点 ID" /><button type="button" [disabled]="updatingNode()" (click)="applyBulkNodeAction('move')">移动栏目</button>
  34. } @else if (className() === 'Special') {
  35. <input class="bulk-node" type="number" min="0" [ngModel]="targetSpecialParentId()" (ngModelChange)="targetSpecialParentId.set(+$event)" placeholder="父专题 ID" /><button type="button" [disabled]="updatingSpecial()" (click)="applyBulkSpecialMove()">移动专题</button>
  36. } @else if (className() === 'Guestbook') {
  37. <button type="button" [disabled]="updatingGuestbook()" (click)="applyBulkGuestbookAction('audit')">审核</button><button type="button" [disabled]="updatingGuestbook()" (click)="applyBulkGuestbookAction('unaudit')">取消审核</button><button class="danger-action" type="button" [disabled]="updatingGuestbook()" (click)="applyBulkGuestbookAction('recycle')">回收</button><button type="button" [disabled]="updatingGuestbook()" (click)="applyBulkGuestbookAction('recover')">恢复</button>
  38. } @else if (className() === 'Datadiccategory' || className() === 'Datadic') {
  39. <button type="button" [disabled]="updatingDictionary()" (click)="applyBulkDictionaryAction('enable')">启用</button><button type="button" [disabled]="updatingDictionary()" (click)="applyBulkDictionaryAction('disable')">停用</button><button class="danger-action" type="button" [disabled]="updatingDictionary()" (click)="applyBulkDictionaryAction('delete')">删除</button>
  40. } @else if (className() === 'GradeCate' || className() === 'Grade') {
  41. <button class="danger-action" type="button" [disabled]="updatingGrade()" (click)="applyBulkGradeDelete()">删除</button>
  42. } @else if (className() === 'Currency') {
  43. <button class="danger-action" type="button" [disabled]="updatingCurrency()" (click)="applyBulkCurrencyDelete()">删除</button>
  44. } @else if (className() === 'SysHoliday') {
  45. <button class="danger-action" type="button" [disabled]="updatingHoliday()" (click)="applyBulkHolidayDelete()">删除</button>
  46. } @else if (className() === 'Search') {
  47. <button type="button" [disabled]="updatingSearchNavigation()" (click)="applyBulkSearchNavigationAction('enable')">启用</button><button type="button" [disabled]="updatingSearchNavigation()" (click)="applyBulkSearchNavigationAction('disable')">停用</button><button type="button" [disabled]="updatingSearchNavigation()" (click)="applyBulkSearchNavigationAction('elite')">推荐</button><button type="button" [disabled]="updatingSearchNavigation()" (click)="applyBulkSearchNavigationAction('unelite')">取消推荐</button><input class="bulk-node" type="number" min="1" max="1000000" [ngModel]="searchNavigationOrderStart()" (ngModelChange)="searchNavigationOrderStart.set(+$event)" placeholder="起始排序" /><button type="button" [disabled]="updatingSearchNavigation()" (click)="applyBulkSearchNavigationAction('order')">按选择顺序排列</button><button class="danger-action" type="button" [disabled]="updatingSearchNavigation()" (click)="applyBulkSearchNavigationAction('delete')">删除</button>
  48. } @else if (className() === 'AdZone') {
  49. <button type="button" [disabled]="updatingAdvertisement()" (click)="applyBulkAdvertisementAction('active')">启用</button><button type="button" [disabled]="updatingAdvertisement()" (click)="applyBulkAdvertisementAction('pause')">停用</button><button class="danger-action" type="button" [disabled]="updatingAdvertisement()" (click)="applyBulkAdvertisementAction('delete')">删除</button>
  50. } @else if (className() === 'AdInfo') {
  51. <button type="button" [disabled]="updatingAdvertisement()" (click)="applyBulkAdvertisementAction('audit')">审核</button><button type="button" [disabled]="updatingAdvertisement()" (click)="applyBulkAdvertisementAction('unaudit')">取消审核</button><button class="danger-action" type="button" [disabled]="updatingAdvertisement()" (click)="applyBulkAdvertisementAction('delete')">删除</button>
  52. } @else if (className() === 'FontPicShape' || className() === 'FontPicShapeType') {
  53. <button class="danger-action" type="button" [disabled]="updatingFontShape()" (click)="applyBulkFontShapeDelete()">删除</button>
  54. } @else {
  55. <button type="button" [disabled]="updatingBar()" (click)="applyBulkBarAction('audit')">审核</button><button class="danger-action" type="button" [disabled]="updatingBar()" (click)="applyBulkBarAction('recycle')">回收</button><button type="button" [disabled]="updatingBar()" (click)="applyBulkBarAction('recover')">恢复</button><button type="button" [disabled]="updatingBar()" (click)="applyBulkBarAction('elite')">加精</button><button type="button" [disabled]="updatingBar()" (click)="applyBulkBarAction('top-section')">版块置顶</button><button type="button" [disabled]="updatingBar()" (click)="applyBulkBarAction('top-global')">全局置顶</button><button type="button" [disabled]="updatingBar()" (click)="applyBulkBarAction('top-undo')">取消排序</button>
  56. <input class="bulk-node" type="number" min="1" [ngModel]="targetBarCategoryId()" (ngModelChange)="targetBarCategoryId.set(+$event)" placeholder="版块 ID" /><button type="button" [disabled]="updatingBar() || !targetBarCategoryId()" (click)="applyBulkBarAction('move')">移动版块</button>
  57. }
  58. </div>
  59. }
  60. @if (bulkError()) { <div class="bulk-notice error">{{ bulkError() }}</div> }
  61. @if (bulkMessage()) { <div class="bulk-notice success">{{ bulkMessage() }}</div> }
  62. @if (error()) { <div class="inline-error">{{ error() }}</div> }
  63. @if (loading() && !page()) {
  64. <div class="empty"><lucide-icon class="spin" [img]="icons.LoaderCircle" [size]="26" />正在加载数据…</div>
  65. } @else {
  66. <div class="table-scroll"><table><thead><tr>@if (bulkEnabled()) { <th class="select-col"><input type="checkbox" aria-label="全选当前页" [checked]="allPageSelected()" (change)="togglePageSelection($any($event.target).checked)" /></th> }@for (field of columns(); track field.name) { <th>{{ field.name }}</th> }<th class="actions">操作</th></tr></thead><tbody>
  67. @for (row of page()?.results || []; track row['objectId']) {
  68. <tr>@if (bulkEnabled()) { <td class="select-col"><input type="checkbox" [attr.aria-label]="'选择 ' + row['objectId']" [checked]="isSelected(row['objectId'])" (change)="toggleSelection('' + row['objectId'], $any($event.target).checked)" /></td> }@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 && !['Model', 'ModelField'].includes(className())) { <button class="danger" type="button" [attr.aria-label]="['CommonModel', 'Node', 'Guestbook'].includes(className()) ? '移入回收站' : '删除'" (click)="remove(row)"><lucide-icon [img]="icons.Trash2" [size]="16" /></button> }</td></tr>
  69. } @empty { <tr><td class="empty-cell" [attr.colspan]="columns().length + (bulkEnabled() ? 2 : 1)">没有符合条件的数据</td></tr> }
  70. </tbody></table></div>
  71. <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>
  72. }
  73. </section>
  74. @if (editorOpen()) {
  75. <button class="drawer-mask" type="button" aria-label="关闭编辑器" (click)="closeEditor()"></button>
  76. <aside class="editor-drawer">
  77. <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>
  78. <div class="editor-fields">
  79. @if (className() === '_User' && !draftId()) {
  80. <section class="account-create">
  81. <div class="account-create-note"><strong>开通普通用户</strong><span>自动归入当前帐套与默认组 1,并初始化旧系统用户 ID、余额和经验字段。此入口不能授予管理员权限。</span></div>
  82. <label><span>登录账号 <small>3–64 位 · 必填</small></span><input autocomplete="off" [ngModel]="createUsername()" (ngModelChange)="createUsername.set($event)" /></label>
  83. <label><span>显示名 <small>选填</small></span><input autocomplete="off" [ngModel]="createDisplayName()" (ngModelChange)="createDisplayName.set($event)" /></label>
  84. <label><span>登录密码 <small>8–72 位 · 必填</small></span><input type="password" autocomplete="new-password" [ngModel]="createPassword()" (ngModelChange)="createPassword.set($event)" /></label>
  85. <label><span>确认密码 <small>必填</small></span><input type="password" autocomplete="new-password" [ngModel]="createConfirmPassword()" (ngModelChange)="createConfirmPassword.set($event)" /></label>
  86. <label><span>手机号 <small>选填</small></span><input inputmode="tel" autocomplete="off" [ngModel]="createMobile()" (ngModelChange)="createMobile.set($event)" /></label>
  87. <label><span>邀请账号 <small>选填 · 同帐套</small></span><input autocomplete="off" [ngModel]="createInviteUsername()" (ngModelChange)="createInviteUsername.set($event)" /></label>
  88. @if (createUserError()) { <div class="account-create-error">{{ createUserError() }}</div> }
  89. </section>
  90. } @else {
  91. @if (className() === 'Model' || className() === 'ModelField') {
  92. <section class="account-create-note"><strong>仅编辑安全元数据</strong><span>模型编号、表名、字段名、字段类型和排序属于数据库结构,已锁定。新增、删除、复制或改变字段类型需要 PostgreSQL DDL 与旧模板运行时,当前会明确返回迁移阻塞。</span></section>
  93. }
  94. @if (className() === 'Search') {
  95. <section class="account-create-note"><strong>现有快捷入口编辑</strong><span>旧数字 ID 在目标兼容层不可写,因此仅允许编辑现有入口;启停、推荐和排序请在列表批量工具中操作。旧全局后台起始页依赖的 SiteConfig 持久层已不存在。</span></section>
  96. }
  97. @if (className() === 'AdZone' || className() === 'AdInfo') {
  98. <section class="account-create-note"><strong>现有{{ className() === 'AdZone' ? '广告位' : '广告内容' }}编辑</strong><span>旧数字 ID 与 Parse 保留字段冲突,创建探测不会生成旧编号,因此当前只允许安全编辑现有记录;新增请求会返回明确迁移阻塞。</span></section>
  99. }
  100. @if (className() === 'FontPicShape' || className() === 'FontPicShapeType') {
  101. <section class="account-create-note"><strong>现有{{ className() === 'FontPicShape' ? '图形素材' : '图形分类' }}编辑</strong><span>目标兼容层无法生成旧数字 ID,因此仅维护现有记录;新增请求返回明确迁移阻塞。成品字体图和草稿没有目标 Schema,已单独列入数据阻塞。</span></section>
  102. }
  103. @for (field of schema()?.fields || []; track field.name) { @if (field.writable) {
  104. <label><span>{{ field.name }} <small>{{ field.type }}{{ field.required ? ' · 必填' : '' }}</small></span>
  105. @if (field.type === 'Boolean') { <input type="checkbox" [ngModel]="draft()[field.name] === true" (ngModelChange)="setField(field, $event)" /> }
  106. @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> } }
  107. @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" /> }
  108. </label>
  109. } }
  110. @if (className() === 'AdInfo') {
  111. <section class="content-controls ad-info-controls">
  112. <header><div><strong>广告归属</strong><span>只能选择当前帐套真实广告位;名称、素材、链接、时间、权重与审核状态由专用云函数校验</span></div>@if (draftId()) { <b>ID {{ draft()['id'] }}</b> }</header>
  113. <label><span>所属广告位</span><select [ngModel]="targetAdZoneId()" (ngModelChange)="targetAdZoneId.set($event)"><option value="">请选择广告位</option>@for (zone of adZones(); track zone['objectId']) { <option [value]="adZoneIdOf(zone)">{{ adZoneLabel(zone) }}</option> }</select>@if (!adZones().length) { <small>当前帐套没有可用广告位</small> }</label>
  114. </section>
  115. }
  116. @if (className() === 'AdZone' && draftId()) {
  117. <section class="content-controls ad-preview-controls">
  118. <header><div><strong>当前生效广告预览</strong><span>仅展示已启用且处于开始/到期时间范围内的关联广告;HTML 以源码文本显示,避免在管理端执行旧脚本</span></div><b>ID {{ draft()['id'] }}</b></header>
  119. <button type="button" [disabled]="adPreviewLoading()" (click)="loadAdPreview()">{{ adPreviewLoading() ? '加载中…' : '刷新预览' }}</button>
  120. @if (adPreviewError()) { <div class="content-action-status error">{{ adPreviewError() }}</div> }
  121. <div class="ad-preview-list">@for (ad of adPreview(); track ad['objectId']) { <article><strong>{{ ad['name'] }}</strong><span>权重 {{ ad['priority'] || 0 }} · {{ display(ad['sdate']) }} 至 {{ display(ad['edate']) }}</span>@if (ad['image'] || ad['images']) { <code>{{ ad['image'] || ad['images'] }}</code> }@if (ad['text']) { <p>{{ ad['text'] }}</p> }@if (ad['html']) { <pre>{{ ad['html'] }}</pre> }</article> } @empty { @if (!adPreviewLoading() && !adPreviewError()) { <div class="content-action-status">当前没有生效广告</div> } }</div>
  122. </section>
  123. }
  124. @if (className() === 'FontPicShape') {
  125. <section class="content-controls">
  126. <header><div><strong>图形素材与分类</strong><span>可维护名称、备注和分类;原图路径只读,图片替换等待服务端文件存储适配器修复</span></div>@if (draftId()) { <b>ID {{ draft()['id'] }}</b> }</header>
  127. <label><span>所属图形分类</span><select [ngModel]="targetFontShapeTypeId()" (ngModelChange)="targetFontShapeTypeId.set(+$event)"><option [ngValue]="0">请选择分类</option>@for (type of fontShapeTypes(); track type['objectId']) { <option [ngValue]="fontShapeTypeIdOf(type)">{{ fontShapeTypeLabel(type) }}</option> }</select></label>
  128. @if (fontShapePreview()) { <div><img [src]="fontShapePreview()" alt="图形素材预览" width="120" height="120" /><code>{{ fontShapePreview() }}</code></div> }
  129. <div class="content-action-status error">图片替换暂不可用:当前 Parse Server 的 SDK 与 /files REST 接口都错误要求 MongoDB 连接串;本项目数据库为 PostgreSQL。云函数会对此返回明确 501,不会伪装上传成功。</div>
  130. </section>
  131. }
  132. @if (className() === 'CommonModel' && draftId()) {
  133. <section class="content-controls">
  134. <header><div><strong>内容审核与归档</strong><span>专用云函数同步旧 ContentManage 状态与节点移动语义</span></div><b [class.recycled]="contentStatusValue() === -2">{{ contentStatusLabel(contentStatusValue()) }}</b></header>
  135. <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>
  136. <button type="button" [disabled]="updatingContent()" (click)="applyContentAction('status')">更新状态</button>
  137. <label><span>目标节点 ID</span><input type="number" min="1" [ngModel]="targetContentNodeId()" (ngModelChange)="targetContentNodeId.set(+$event)" /></label>
  138. <button type="button" [disabled]="updatingContent() || !targetContentNodeId()" (click)="applyContentAction('move')">移动节点</button>
  139. <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>
  140. @if (contentActionError()) { <div class="content-action-status error">{{ contentActionError() }}</div> }
  141. @if (contentActionMessage()) { <div class="content-action-status success">{{ contentActionMessage() }}</div> }
  142. </section>
  143. }
  144. @if (className() === 'Node') {
  145. <section class="content-controls node-controls">
  146. <header><div><strong>栏目结构与回收站</strong><span>专用云函数维护栏目编号、父子层级、同级防重与循环保护</span></div>@if (draftId()) { <b [class.recycled]="nodeStatusValue() === -2">{{ nodeStatusValue() === -2 ? '回收站' : '正常' }} · ID {{ draft()['nodeId'] }}</b> }</header>
  147. <label><span>父节点 ID <small>顶级栏目填 0</small></span><input type="number" min="0" [ngModel]="targetNodeParentId()" (ngModelChange)="targetNodeParentId.set(+$event)" /></label>
  148. @if (draftId()) {
  149. <button type="button" [disabled]="updatingNode()" (click)="applyNodeAction('move')">移动栏目</button>
  150. <div class="content-recycle">@if (nodeStatusValue() === -2) { <button type="button" [disabled]="updatingNode()" (click)="applyNodeAction('recover')">从回收站恢复</button> } @else { <button class="danger-action" type="button" [disabled]="updatingNode()" (click)="applyNodeAction('recycle')">移入回收站</button> }</div>
  151. @if (nodeActionError()) { <div class="content-action-status error">{{ nodeActionError() }}</div> }
  152. @if (nodeActionMessage()) { <div class="content-action-status success">{{ nodeActionMessage() }}</div> }
  153. }
  154. </section>
  155. }
  156. @if (className() === 'Special') {
  157. <section class="content-controls special-controls">
  158. <header><div><strong>专题结构</strong><span>专用云函数维护专题编号、全帐套名称/目录防重与父子循环保护</span></div>@if (draftId()) { <b>ID {{ draft()['specId'] }}</b> }</header>
  159. <label><span>父专题 ID <small>顶级专题填 0</small></span><input type="number" min="0" [ngModel]="targetSpecialParentId()" (ngModelChange)="targetSpecialParentId.set(+$event)" /></label>
  160. @if (draftId()) {
  161. <button type="button" [disabled]="updatingSpecial()" (click)="moveSpecial()">移动专题</button>
  162. @if (specialActionError()) { <div class="content-action-status error">{{ specialActionError() }}</div> }
  163. @if (specialActionMessage()) { <div class="content-action-status success">{{ specialActionMessage() }}</div> }
  164. }
  165. </section>
  166. }
  167. @if (className() === 'Guestcate') {
  168. <section class="content-controls guest-category-controls">
  169. <header><div><strong>留言 / 贴吧分类结构</strong><span>专用云函数维护原子 cateid、分类类型、父子循环和引用保护</span></div>@if (draftId()) { <b>ID {{ draft()['cateid'] }}</b> }</header>
  170. <label><span>分类用途</span><select [disabled]="!!draftId()" [ngModel]="guestCategoryType()" (ngModelChange)="guestCategoryType.set(+$event); targetGuestCategoryParentId.set(0)"><option [ngValue]="0">留言分类</option><option [ngValue]="1">贴吧版块</option></select></label>
  171. <label><span>父分类 ID <small>留言分类固定为 0</small></span><input type="number" min="0" [disabled]="guestCategoryType() === 0" [ngModel]="targetGuestCategoryParentId()" (ngModelChange)="targetGuestCategoryParentId.set(+$event)" /></label>
  172. </section>
  173. }
  174. @if (className() === 'ExamClass') {
  175. <section class="content-controls exam-class-controls">
  176. <header><div><strong>试题分类结构</strong><span>专用云函数维护原子分类编号、父子循环、名称防重和试题引用保护</span></div>@if (draftId()) { <b>ID {{ draft()['cId'] }}</b> }</header>
  177. <label><span>父分类 ID <small>顶级分类填 0</small></span><input type="number" min="0" [ngModel]="targetExamClassParentId()" (ngModelChange)="targetExamClassParentId.set(+$event)" /></label>
  178. <label><span>分类类型</span><select [ngModel]="examClassType()" (ngModelChange)="examClassType.set(+$event)"><option [ngValue]="1">答题类型</option><option [ngValue]="2">视频操作类型</option></select></label>
  179. <label><span>排序 <small>新建填 0 自动顺排</small></span><input type="number" min="0" [ngModel]="examClassOrder()" (ngModelChange)="examClassOrder.set(+$event)" /></label>
  180. </section>
  181. }
  182. @if (className() === 'ExamPoint') {
  183. <section class="content-controls exam-point-controls">
  184. <header><div><strong>考点结构</strong><span>专用云函数维护原子考点编号、父子循环、名称防重和下级引用保护</span></div>@if (draftId()) { <b>ID {{ draft()['id'] }}</b> }</header>
  185. <label><span>上级考点 ID <small>顶级考点填 0</small></span><input type="number" min="0" [ngModel]="targetExamPointParentId()" (ngModelChange)="targetExamPointParentId.set(+$event)" /></label>
  186. <label><span>排序 <small>新建填 0 自动顺排</small></span><input type="number" min="0" [ngModel]="examPointOrder()" (ngModelChange)="examPointOrder.set(+$event)" /></label>
  187. </section>
  188. }
  189. @if (className() === 'QuestionsKnowledge') {
  190. <section class="content-controls knowledge-controls">
  191. <header><div><strong>知识点结构</strong><span>专用云函数维护原子知识点编号、所属试题分类、父子循环和试题引用保护</span></div>@if (draftId()) { <b>ID {{ draft()['kId'] }}</b> }</header>
  192. <label><span>试题分类 ID</span><input type="number" min="1" [ngModel]="targetKnowledgeClassId()" (ngModelChange)="targetKnowledgeClassId.set(+$event)" /></label>
  193. <label><span>父知识点 ID <small>顶级知识点填 0</small></span><input type="number" min="0" [ngModel]="targetKnowledgeParentId()" (ngModelChange)="targetKnowledgeParentId.set(+$event)" /></label>
  194. <label><span>排序 <small>新建填 0 自动顺排</small></span><input type="number" min="0" [ngModel]="knowledgeOrder()" (ngModelChange)="knowledgeOrder.set(+$event)" /></label>
  195. </section>
  196. }
  197. @if (className() === 'ExTeacher') {
  198. <section class="content-controls exam-teacher-controls">
  199. <header><div><strong>考试教师资料</strong><span>专用云函数维护原子教师编号、创建管理员和任教试题分类归属</span></div>@if (draftId()) { <b>ID {{ draft()['id'] }}</b> }</header>
  200. <label><span>任教试题分类 ID <small>未指定填 0</small></span><input type="number" min="0" [ngModel]="targetTeacherClassId()" (ngModelChange)="targetTeacherClassId.set(+$event)" /></label>
  201. </section>
  202. }
  203. @if (className() === 'Datadiccategory') {
  204. <section class="content-controls dictionary-controls">
  205. <header><div><strong>数据字典分类</strong><span>专用云函数维护旧 DicCateID、帐套内名称防重和字典项引用保护</span></div>@if (draftId()) { <b>ID {{ draft()['diccateid'] }}</b> }</header>
  206. </section>
  207. }
  208. @if (className() === 'Datadic') {
  209. <section class="content-controls dictionary-controls">
  210. <header><div><strong>数据字典项</strong><span>专用云函数维护旧 DicID,并只允许挂到当前帐套真实存在的分类</span></div>@if (draftId()) { <b>ID {{ draft()['dicid'] }}</b> }</header>
  211. <label><span>所属字典分类</span><select [ngModel]="targetDictionaryCategoryId()" (ngModelChange)="targetDictionaryCategoryId.set(+$event)"><option [ngValue]="0">请选择分类</option>@for (category of dictionaryCategories(); track category['objectId']) { <option [ngValue]="dictionaryCategoryIdOf(category)">{{ category['categoryname'] }}({{ category['diccateid'] }}){{ category['isused'] === false ? ' · 已停用' : '' }}</option> }</select>@if (!dictionaryCategories().length) { <small>当前帐套尚无可用分类,请先从“数据字典分类”建立分类</small> }</label>
  212. </section>
  213. }
  214. @if (className() === 'GradeCate') {
  215. <section class="content-controls grade-category-controls">
  216. <header><div><strong>多级字典分类</strong><span>层级别名使用“|”或换行分隔,至少定义两级;专用云函数维护旧 CateID 和引用保护</span></div>@if (draftId()) { <b>ID {{ draft()['cateId'] }}</b> }</header>
  217. </section>
  218. }
  219. @if (className() === 'Grade') {
  220. <section class="content-controls grade-option-controls">
  221. <header><div><strong>多级字典选项</strong><span>新建时按父选项自动推导层级;编辑时保持原分类、父级和层级不变</span></div>@if (draftId()) { <b>ID {{ draft()['gradeId'] }} · 第 {{ draft()['grade'] }} 级</b> }</header>
  222. <label><span>所属多级字典分类</span><select [disabled]="!!draftId()" [ngModel]="targetGradeCategoryId()" (ngModelChange)="targetGradeCategoryId.set(+$event)"><option [ngValue]="0">请选择分类</option>@for (category of gradeCategories(); track category['objectId']) { <option [ngValue]="gradeCategoryIdOf(category)">{{ category['cateName'] }}({{ category['cateId'] }})</option> }</select></label>
  223. <label><span>父选项 ID <small>第一级填 0</small></span><input type="number" min="0" [disabled]="!!draftId()" [ngModel]="targetGradeParentId()" (ngModelChange)="targetGradeParentId.set(+$event)" /></label>
  224. </section>
  225. }
  226. @if (className() === 'Guestbook' && draftId()) {
  227. <section class="content-controls guestbook-controls">
  228. <header><div><strong>留言审核与管理员回复</strong><span>专用云函数维护旧 status、父留言关系和回复 GID</span></div><b [class.recycled]="selectedGuestbookStatus() === -2">{{ selectedGuestbookStatus() === 99 ? '已审核' : selectedGuestbookStatus() === -2 ? '回收站' : '待审核' }}</b></header>
  229. <button type="button" [disabled]="updatingGuestbook()" (click)="applyGuestbookAction('audit')">审核</button><button type="button" [disabled]="updatingGuestbook()" (click)="applyGuestbookAction('unaudit')">取消审核</button>
  230. <div class="content-recycle">@if (selectedGuestbookStatus() === -2) { <button type="button" [disabled]="updatingGuestbook()" (click)="applyGuestbookAction('recover')">从回收站恢复</button> } @else { <button class="danger-action" type="button" [disabled]="updatingGuestbook()" (click)="applyGuestbookAction('recycle')">移入回收站</button> }</div>
  231. <label><span>回复标题 <small>选填</small></span><input [ngModel]="guestbookReplyTitle()" (ngModelChange)="guestbookReplyTitle.set($event)" placeholder="[管理员回复]" /></label>
  232. <label><span>回复内容 <small>必填</small></span><textarea rows="5" [ngModel]="guestbookReplyContent()" (ngModelChange)="guestbookReplyContent.set($event)"></textarea></label>
  233. <button type="button" [disabled]="updatingGuestbook() || !guestbookReplyContent().trim()" (click)="replyGuestbook()">发布管理员回复</button>
  234. @if (guestbookActionError()) { <div class="content-action-status error">{{ guestbookActionError() }}</div> }
  235. @if (guestbookActionMessage()) { <div class="content-action-status success">{{ guestbookActionMessage() }}</div> }
  236. </section>
  237. }
  238. @if (className() === 'GuestBar' && draftId()) {
  239. <section class="content-controls bar-controls">
  240. <header><div><strong>贴吧帖子工作流</strong><span>审核、回收、精华、置顶/沉底和移动均由专用云函数维护</span></div><b [class.recycled]="draft()['status'] === -2">{{ draft()['status'] === -2 ? '回收站' : draft()['status'] === 99 ? '已审核' : '待审核' }}</b></header>
  241. <button type="button" [disabled]="updatingBar()" (click)="applyBarAction('audit')">审核</button><button type="button" [disabled]="updatingBar()" (click)="applyBarAction('unaudit')">取消审核</button>
  242. <button type="button" [disabled]="updatingBar()" (click)="applyBarAction('elite')">设为精华</button><button type="button" [disabled]="updatingBar()" (click)="applyBarAction('elite-undo')">取消精华</button>
  243. <button type="button" [disabled]="updatingBar()" (click)="applyBarAction('top-section')">版块置顶</button><button type="button" [disabled]="updatingBar()" (click)="applyBarAction('top-global')">全局置顶</button>
  244. <button type="button" [disabled]="updatingBar()" (click)="applyBarAction('bottom')">沉底</button><button type="button" [disabled]="updatingBar()" (click)="applyBarAction('top-undo')">取消排序</button>
  245. <label><span>目标贴吧分类 ID</span><input type="number" min="1" [ngModel]="targetBarCategoryId()" (ngModelChange)="targetBarCategoryId.set(+$event)" /></label><button type="button" [disabled]="updatingBar() || !targetBarCategoryId()" (click)="applyBarAction('move')">移动版块</button>
  246. <div class="content-recycle">@if (draft()['status'] === -2) { <button type="button" [disabled]="updatingBar()" (click)="applyBarAction('recover')">从回收站恢复</button> } @else { <button class="danger-action" type="button" [disabled]="updatingBar()" (click)="applyBarAction('recycle')">移入回收站</button> }</div>
  247. @if (barActionError()) { <div class="content-action-status error">{{ barActionError() }}</div> }
  248. @if (barActionMessage()) { <div class="content-action-status success">{{ barActionMessage() }}</div> }
  249. </section>
  250. }
  251. @if (className() === '_User' && draftId()) {
  252. <section class="user-controls">
  253. <header><div><strong>账号状态与用户组</strong><span>专用云函数会同步旧版 State / GroupID;停用账号会立即撤销会话</span></div><b [class.disabled]="draft()['isDisabled'] === true">{{ draft()['isDisabled'] === true ? '已停用' : '正常' }}</b></header>
  254. <label class="reason-field"><span>状态变更原因 <small>选填 · 最多 200 字</small></span><input [ngModel]="userActionReason()" (ngModelChange)="userActionReason.set($event)" maxlength="200" /></label>
  255. <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>
  256. <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>
  257. <button class="group-submit" type="button" [disabled]="updatingUser() || !selectedUserGroupId()" (click)="moveUserGroup()">保存用户组</button>
  258. @if (userActionError()) { <div class="user-action-status error">{{ userActionError() }}</div> }
  259. @if (userActionMessage()) { <div class="user-action-status success">{{ userActionMessage() }}</div> }
  260. </section>
  261. <section class="password-reset">
  262. <header><lucide-icon [img]="icons.KeyRound" [size]="18" /><div><strong>重置登录密码</strong><span>通过专用云函数写入,不进入通用资料字段</span></div></header>
  263. <label><span>新密码 <small>至少 8 位</small></span><input type="password" autocomplete="new-password" [ngModel]="newPassword()" (ngModelChange)="newPassword.set($event)" /></label>
  264. <label><span>确认密码</span><input type="password" autocomplete="new-password" [ngModel]="confirmPassword()" (ngModelChange)="confirmPassword.set($event)" /></label>
  265. @if (passwordError()) { <div class="password-status error">{{ passwordError() }}</div> }
  266. @if (passwordMessage()) { <div class="password-status success">{{ passwordMessage() }}</div> }
  267. <button type="button" [disabled]="resettingPassword()" (click)="resetPassword()">{{ resettingPassword() ? '重置中…' : '确认重置密码' }}</button>
  268. </section>
  269. }
  270. }
  271. </div>
  272. <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>
  273. </aside>
  274. }