@yss123/cage-admin 0.3.0 → 0.3.1

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/dist/catalog.d.ts CHANGED
@@ -31,6 +31,7 @@ export declare function adminCatalog(registry: Registry, labels?: AdminLabels):
31
31
  sensitive: Record<string, string>;
32
32
  }[];
33
33
  combinators: string[];
34
+ approvalOperations: boolean;
34
35
  approvals: {
35
36
  name: string;
36
37
  process: string;
package/dist/catalog.js CHANGED
@@ -76,6 +76,8 @@ export function adminCatalog(registry, labels = {}) {
76
76
  capabilities,
77
77
  aggregates,
78
78
  combinators: ['self', 'dept', 'deptSubtree', 'deptSet', 'all'],
79
+ // 功能发现只控制通用界面入口;实际催办与提醒读写仍在审批服务中逐对象授权。
80
+ approvalOperations: registry.extensions.some(e => e.kind === 'RUNTIME' && e.value.name === 'approval-operations'),
79
81
  approvals: registry.approvals.map(({ a, name }) => ({
80
82
  name,
81
83
  process: a.process,
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yss123/cage-admin",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "组织模型声明与 /admin 系统管理",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -25,8 +25,8 @@
25
25
  "fastify": "^5.6.0",
26
26
  "zod": "^4.1.5",
27
27
  "kysely": "^0.28.5",
28
- "@yss123/cage-kernel": "0.3.0",
29
- "@yss123/cage-ai-protocol": "0.3.0"
28
+ "@yss123/cage-ai-protocol": "0.3.1",
29
+ "@yss123/cage-kernel": "0.3.1"
30
30
  },
31
31
  "license": "MIT",
32
32
  "repository": {
@@ -6,7 +6,7 @@
6
6
  import { api, can, run, state } from '../shared/api.js';
7
7
  import { emptyState, h, icon, panel } from '../shared/dom.js';
8
8
  import { navigate } from '../shared/navigation.js';
9
- import { renderTasks } from '../modules/tasks/index.js';
9
+ import { renderTasks, renderApprovalRequests, renderApprovalNotices } from '../modules/tasks/index.js';
10
10
  import { renderOrg } from '../modules/org/index.js';
11
11
  import { renderRoles } from '../modules/roles/index.js';
12
12
  import { renderAudit } from '../modules/audit/index.js';
@@ -19,6 +19,14 @@ import { renderPersonalities } from '../modules/personalities/index.js';
19
19
  * 这些条目同时决定导航显示和页面级提示,不能替代模块请求或服务端鉴权。
20
20
  */
21
21
  const pages = {
22
+ requests: {
23
+ title: '我的申请', description: '跟进本人发起的审批,查看办理时限并向当前办理人催办。',
24
+ glyph: 'file', category: '团队与权限', render: renderApprovalRequests, need: [],
25
+ },
26
+ notices: {
27
+ title: '流程提醒', description: '集中查看临期、超时与催办提醒,及时推进审批协作。',
28
+ glyph: 'clock', category: '团队与权限', render: renderApprovalNotices, need: [],
29
+ },
22
30
  personalities: {
23
31
  title: '助理性格', description: '设置助理的语气、表达习惯和默认性格,随时试聊。',
24
32
  glyph: 'users', category: '运行与记录', render: renderPersonalities, need: ['Sys.ai.personalities.manage'],
@@ -89,10 +97,11 @@ export async function render(boot) {
89
97
  root.replaceChildren(loginPage(boot));
90
98
  return;
91
99
  }
92
- const hash = location.hash.replace(/^#\/?/, '') || 'tasks';
100
+ // 页面键和筛选参数分开解析;提醒页的筛选随 hash 保留,刷新与前进后退仍指向同一视图。
101
+ const hash = location.hash.replace(/^#\/?/, '').split('?')[0] || 'tasks';
93
102
  const key = (hash in pages ? hash : 'tasks');
94
103
  const page = pages[key];
95
- const visible = Object.entries(pages).filter(([, item]) => !item.need.length || item.need.some(can));
104
+ const visible = Object.entries(pages).filter(([name, item]) => (!['requests', 'notices'].includes(name) || state.catalog?.approvalOperations) && (!item.need.length || item.need.some(can)));
96
105
  const sidebar = h('aside', { class: 'app-sidebar' }, h('a', { class: 'brand', href: '#/tasks', 'aria-label': 'Cage 系统管理首页' }, h('span', { class: 'brand-mark' }, icon('grid')), h('span', {}, 'Cage', h('small', {}, '系统管理'))), ...['团队与权限', '运行与记录'].map((category) => h('div', { class: 'nav-group' }, h('p', { class: 'nav-caption' }, category), h('nav', { 'aria-label': category }, visible
97
106
  .filter(([, item]) => item.category === category)
98
107
  .map(([name, item]) => h('a', {
@@ -116,7 +125,7 @@ export async function render(boot) {
116
125
  const content = h('div', { class: 'page-content' });
117
126
  const loading = h('div', { class: 'loading-panel', role: 'status' }, h('span', { class: 'spinner' }), '正在读取管理数据…');
118
127
  content.append(loading);
119
- const main = h('main', { class: 'app-main' }, h('header', { class: 'app-topbar' }, h('div', { class: 'breadcrumb' }, h('span', {}, '工作空间'), icon('chevron'), h('span', {}, '系统管理'), icon('chevron'), h('strong', {}, page.title)), h('div', { class: 'account' }, h('span', { class: 'avatar' }, state.me.user.name.slice(0, 1)), h('span', { class: 'account-copy' }, h('strong', {}, state.me.user.name), h('small', {}, '当前登录')), logout)), h('div', { class: 'page-container' }, h('div', { class: 'page-heading' }, h('div', {}, h('span', { class: 'eyebrow' }, '管理工作台'), h('h1', {}, page.title), h('p', {}, page.description)), h('button', { class: 'refresh-button', onclick: () => navigate(key) }, icon('refresh'), '刷新数据')), content, h('footer', { class: 'page-footer' }, h('span', {}, 'Cage · 让业务有序发生'), h('span', {}, icon('shield'), '操作受角色权限与数据范围约束'))));
128
+ const main = h('main', { class: 'app-main' }, h('header', { class: 'app-topbar' }, h('div', { class: 'breadcrumb' }, h('span', {}, '工作空间'), icon('chevron'), h('span', {}, '系统管理'), icon('chevron'), h('strong', {}, page.title)), h('div', { class: 'account' }, h('span', { class: 'avatar' }, state.me.user.name.slice(0, 1)), h('span', { class: 'account-copy' }, h('strong', {}, state.me.user.name), h('small', {}, '当前登录')), logout)), h('div', { class: 'page-container' }, h('div', { class: 'page-heading' }, h('div', {}, h('span', { class: 'eyebrow' }, '管理工作台'), h('h1', {}, page.title), h('p', {}, page.description)), h('button', { class: 'refresh-button', onclick: () => navigate(location.hash.replace(/^#\/?/, '') || key) }, icon('refresh'), '刷新数据')), content, h('footer', { class: 'page-footer' }, h('span', {}, 'Cage · 让业务有序发生'), h('span', {}, icon('shield'), '操作受角色权限与数据范围约束'))));
120
129
  root.replaceChildren(h('div', { class: 'admin-shell' }, sidebar, main));
121
130
  if (page.need.length && !page.need.some(can)) {
122
131
  loading.remove();
@@ -127,7 +136,7 @@ export async function render(boot) {
127
136
  await page.render(content);
128
137
  }
129
138
  catch (error) {
130
- content.append(h('div', { class: 'error-panel', role: 'alert' }, icon('info'), h('div', {}, h('strong', {}, '暂时无法读取数据'), h('p', {}, error instanceof TypeError ? '请检查网络与 API 服务后重新尝试。' : error.message)), h('button', { onclick: () => navigate(key) }, '重新加载')));
139
+ content.append(h('div', { class: 'error-panel', role: 'alert' }, icon('info'), h('div', {}, h('strong', {}, '暂时无法读取数据'), h('p', {}, error instanceof TypeError ? '请检查网络与 API 服务后重新尝试。' : error.message)), h('button', { onclick: () => navigate(location.hash.replace(/^#\/?/, '') || key) }, '重新加载')));
131
140
  }
132
141
  finally {
133
142
  loading.remove();
@@ -3,3 +3,4 @@
3
3
  * 应用壳通过这里组合页面,其他模块无需依赖内部表单实现;导出本身不发请求或执行管理操作。
4
4
  */
5
5
  export { renderTasks } from './tasks-page.js';
6
+ export { renderApprovalRequests, renderApprovalNotices } from './operations-pages.js';
@@ -0,0 +1,107 @@
1
+ /**
2
+ * 框架自带的申请催办和站内提醒页面,任何启用审批运营的应用都可从 /admin 直接使用。
3
+ * 页面仅消费通用审批 HTTP 协议,不依赖业务领域、采购字段或数据库;状态以服务端返回为准。
4
+ * 过滤保存在 hash 查询参数,写操作完成后刷新当前页;纯文本 DOM 渲染避免把催办原因解释成 HTML。
5
+ */
6
+ import { api, run, state, toast } from '../../shared/api.js';
7
+ import { badge, emptyState, formatTime, h, panel, table } from '../../shared/dom.js';
8
+ import { objectName } from '../../shared/labels.js';
9
+ import { navigate } from '../../shared/navigation.js';
10
+ /** 显示节点总截止时间和浏览器当前时刻的提示;后端会独立判定,不把显示时差作为决定条件。 */
11
+ export function deadlineCell(dueAt) {
12
+ if (!dueAt)
13
+ return h('small', {}, '未设时限');
14
+ const late = Date.parse(dueAt) <= Date.now();
15
+ return h('div', { class: 'stack-cell' }, badge(late ? '已超时' : '办理中', late ? 'red' : 'neutral'), h('small', {}, formatTime(dueAt)));
16
+ }
17
+ /** 目录显式声明未启用时提供说明并停止加载,不将不存在的接口报错伪装为无提醒。 */
18
+ function enabled(main) {
19
+ if (state.catalog?.approvalOperations)
20
+ return true;
21
+ main.append(panel('流程协作', undefined, emptyState('当前应用未启用流程提醒', '启用后可在此查看站内提醒及本人申请。')));
22
+ return false;
23
+ }
24
+ /** 使用聚合中文名与完整单据标识,跨业务通用;不猜测应用自定义的详情 URL。 */
25
+ function subject(item) {
26
+ return h('div', { class: 'stack-cell' }, badge(objectName(item.aggregate)), h('code', { title: item.businessKey }, item.businessKey));
27
+ }
28
+ /** 刷新只重读当前页面;没有后台定时器,离开页面后不会遗留轮询或自动重放催办。 */
29
+ function refresh(page) {
30
+ return h('button', { class: 'small', onclick: () => navigate(page) }, '刷新');
31
+ }
32
+ /**
33
+ * 展示本人运行中的可催办申请。提交绑定实例 UUID 和当前步骤,原因必填,防止旧页面催办新轮次。
34
+ * 冷却期间明确给出下次时间;按钮只是提示,后端仍锁定复核。成功后显示真实接收人数并重新读取。
35
+ */
36
+ export async function renderApprovalRequests(main) {
37
+ main.classList.add('approval-workspace');
38
+ if (!enabled(main))
39
+ return;
40
+ const items = await api('GET', '/api/approval/requests');
41
+ main.append(panel('我的申请', '查看正在审批的申请,向当前办理人发送站内催办。节点时限包含领取、委派和加签所用时间。', h('div', { class: 'table-toolbar' }, h('span', {}, `当前显示 ${items.length} 项`), refresh('requests')), table([
42
+ ['申请事项', item => subject(item)],
43
+ ['当前环节', item => h('div', { class: 'stack-cell' }, ...(item.activeSteps?.map(node => h('div', { class: 'stack-cell' }, h('strong', {}, node.title), h('small', {}, node.branchPath.map(part => `${part.parallel} · ${part.branch}`).join(' / ') || '主线审批')))
44
+ ?? [h('strong', {}, item.title)]))],
45
+ ['最近截止', item => deadlineCell(item.dueAt)],
46
+ ['催办', item => {
47
+ const cooling = !!item.nextUrgeAt && Date.parse(item.nextUrgeAt) > Date.now();
48
+ const reason = h('input', { 'aria-label': `${item.title}催办原因`, placeholder: '请说明需要加快处理的原因', maxlength: 1000, required: true });
49
+ const button = h('button', { class: 'small primary', disabled: cooling, onclick: async (event) => {
50
+ reason.setCustomValidity(reason.value.trim() ? '' : '请填写催办原因');
51
+ if (!reason.reportValidity())
52
+ return;
53
+ const result = await run(() => api('POST', `/api/approval/instances/${item.id}/urge`, { expectedStep: item.currentStep, ...(item.currentSteps ? { expectedSteps: item.currentSteps } : {}), reason: reason.value.trim() }), undefined, event.currentTarget);
54
+ if (result) {
55
+ toast(`已发送 ${result.recipientCount} 条站内催办提醒`);
56
+ navigate('requests');
57
+ }
58
+ } }, cooling ? '冷却中' : '发送催办');
59
+ return h('div', { class: 'task-actions' }, reason, h('div', { class: 'row-actions' }, button), item.nextUrgeAt ? h('small', {}, `可再次催办:${formatTime(item.nextUrgeAt)}${cooling ? ',到时请刷新' : ''}`) : null);
60
+ }],
61
+ ], items, '当前没有可催办的运行申请')));
62
+ }
63
+ /**
64
+ * 分页展示当前仍与本人有关的流程提醒;URL view=unread 固定筛选,可深链接和浏览器前进后退。
65
+ * 已读写入成功才刷新;翻页沿服务端游标,即使权限过滤形成空页也保留“继续加载”。
66
+ * 列表读取不自动标已读,离开页面后忽略正在返回的追加页,避免旧页更新当前画面。
67
+ */
68
+ export async function renderApprovalNotices(main) {
69
+ main.classList.add('approval-workspace');
70
+ if (!enabled(main))
71
+ return;
72
+ const unread = new URLSearchParams(location.hash.split('?')[1] ?? '').get('view') === 'unread';
73
+ const page = `notices?view=${unread ? 'unread' : 'all'}`;
74
+ const query = `/api/approval/notices?limit=50&unreadOnly=${unread}`;
75
+ const initial = await api('GET', query);
76
+ const rows = [...initial.items];
77
+ let next = initial.nextCursor;
78
+ const content = h('div');
79
+ const more = h('button', { class: 'small', onclick: async (event) => {
80
+ if (!next)
81
+ return;
82
+ const result = await run(() => api('GET', `${query}&after=${encodeURIComponent(next)}`), undefined, event.currentTarget);
83
+ if (!result || !content.isConnected)
84
+ return;
85
+ rows.push(...result.items);
86
+ next = result.nextCursor;
87
+ draw();
88
+ } }, '继续加载');
89
+ /** 仅由已完成的读请求重建表格;按钮内容与已读时间始终来自持久状态。 */
90
+ function draw() {
91
+ content.replaceChildren(table([
92
+ ['提醒事项', item => h('div', { class: 'stack-cell' }, badge(item.kind === 'overdue' ? '已超时' : item.kind === 'due_soon' ? '即将到期' : '申请人催办', item.kind === 'overdue' ? 'red' : 'amber'), h('strong', {}, item.title), item.reason ? h('span', {}, item.reason) : h('small', {}, '请及时查看并处理当前审批任务'))],
93
+ ['关联单据', item => subject(item)],
94
+ ['节点截止', item => item.dueAt ? formatTime(item.dueAt) : '未设时限'],
95
+ ['提醒时间', item => formatTime(item.createdAt)],
96
+ ['阅读状态', item => item.readAt ? h('div', { class: 'stack-cell' }, badge('已读', 'green'), h('small', {}, formatTime(item.readAt))) :
97
+ h('button', { class: 'small', onclick: async (event) => {
98
+ const result = await run(() => api('POST', `/api/approval/notices/${item.id}/read`), '已标记为已读', event.currentTarget);
99
+ if (result)
100
+ navigate(page);
101
+ } }, '标记已读')],
102
+ ], rows, unread ? '当前没有可见的未读提醒' : '当前没有可见的流程提醒'));
103
+ more.hidden = !next;
104
+ }
105
+ draw();
106
+ main.append(panel('流程提醒', '临期、超时与申请人催办集中在这里。提醒只提示办理,不会自动作出审批决定。', h('div', { class: 'table-toolbar' }, h('div', { class: 'row-actions' }, h('a', { class: `approval-filter ${unread ? 'selected' : ''}`, href: '#/notices?view=unread', 'aria-current': unread ? 'page' : undefined }, '未读'), h('a', { class: `approval-filter ${!unread ? 'selected' : ''}`, href: '#/notices?view=all', 'aria-current': !unread ? 'page' : undefined }, '全部'), h('a', { class: 'approval-filter', href: '#/tasks' }, '前往待办')), refresh(page)), content, more));
107
+ }
@@ -1,36 +1,45 @@
1
1
  /**
2
- * 当前用户审批待办页面,读取真实任务并通过批准/拒绝接口提交决定。
2
+ * 当前用户审批收件箱,合并本人、可领取和等待加签任务;管理动作与决定均通过宿主会话接口提交。
3
3
  * 待办分配只说明选人,最终动作仍受业务结论命令的能力、范围和硬约束;页面不直接改变单据状态。
4
4
  */
5
5
  import { api, run, state } from '../../shared/api.js';
6
6
  import { badge, emptyState, formatTime, h, panel, table } from '../../shared/dom.js';
7
7
  import { objectName } from '../../shared/labels.js';
8
8
  import { navigate } from '../../shared/navigation.js';
9
+ import { deadlineCell } from './operations-pages.js';
9
10
  /**
10
11
  * 加载本人待办与页面需要的对象资料,展示任务、步骤和可用操作。
11
12
  * 提交期间禁止重复动作,成功后刷新待办,失败保留可理解原因;空列表表示当前请求没有可见待办。
12
13
  */
13
14
  export async function renderTasks(main) {
15
+ main.classList.add('approval-workspace');
14
16
  const hasApprovals = state.catalog.approvals.length > 0;
15
17
  if (!hasApprovals) {
16
18
  main.append(panel('我的审批待办', undefined, emptyState('当前没有启用审批流程', '业务启用审批后,分配给你的任务会显示在这里。')));
17
19
  return;
18
20
  }
19
- const tasks = await api('GET', '/api/approval/tasks');
21
+ const lists = await Promise.all([
22
+ api('GET', '/api/approval/tasks'), api('GET', '/api/approval/candidate-tasks'),
23
+ api('GET', '/api/approval/tasks?status=waiting'),
24
+ ]);
25
+ const tasks = lists.flat();
20
26
  if (!tasks.length) {
21
27
  main.append(panel('我的审批待办', undefined, emptyState('待办已处理完毕', '新的审批任务分配给你后,会显示在这里。')));
22
28
  return;
23
29
  }
24
- main.append(panel('等待你处理', `共 ${tasks.length} 项待办。审批结论会继续执行业务权限与规则校验。`, table([
30
+ main.append(panel('审批收件箱', `待处理 ${lists[0].length} 项 · 可领取 ${lists[1].length} 项 · 等待加签 ${lists[2].length} 项`, table([
25
31
  [
26
32
  '审批事项',
27
- (task) => h('div', { class: 'stack-cell' }, h('strong', {}, task.title), h('small', {}, state.catalog.approvals.find((item) => item.process === task.process)?.label || '业务审批')),
33
+ (task) => h('div', { class: 'stack-cell' }, h('strong', {}, task.title), task.branchPath?.length ? h('small', {}, task.branchPath.map(part => `${part.parallel} · ${part.branch}`).join(' / ')) : null, h('small', {}, state.catalog.approvals.find((item) => item.process === task.process)?.label || '业务审批')),
28
34
  ],
29
35
  [
30
36
  '关联单据',
31
37
  (task) => h('div', { class: 'stack-cell' }, badge(objectName(task.aggregate)), h('code', {}, task.businessKey.slice(0, 8))),
32
38
  ],
33
39
  ['提交时间', (task) => formatTime(task.createdAt)],
40
+ ['办理截止', task => deadlineCell(task.dueAt)],
41
+ ['处理阶段', task => badge(task.status === 'waiting' ? '等待加签' : task.assigneeId === null ? '可领取' : task.delegation === 'pending' ? '受托处理中' :
42
+ task.signPosition === 'before' ? '前签' : task.signPosition === 'after' ? '后签' : task.signPosition === 'parallel' ? '并行加签' : '待处理')],
34
43
  [
35
44
  '流程信息',
36
45
  (task) => h('details', {}, h('summary', {}, '查看详情'), h('pre', {}, JSON.stringify(task.vars, null, 2))),
@@ -38,29 +47,44 @@ export async function renderTasks(main) {
38
47
  [
39
48
  '处理意见与操作',
40
49
  (task) => {
50
+ if (task.status === 'waiting')
51
+ return h('small', {}, '前置签署完成后恢复处理');
41
52
  const comment = h('input', {
42
- placeholder: '填写审批意见(可选)',
53
+ placeholder: '审批意见;领取、释放或归还时必填',
43
54
  'aria-label': `${task.title}审批意见`,
55
+ maxlength: 1000,
44
56
  });
45
57
  const actions = h('div', { class: 'row-actions' });
46
58
  const submit = async (action, button) => {
59
+ const management = ['claim', 'unclaim', 'resolve'].includes(action);
60
+ comment.setCustomValidity(management && !comment.value.trim() ? '请填写操作原因' : '');
61
+ if (!comment.reportValidity())
62
+ return;
47
63
  // 一项待办的两个结论互斥,发送期间同时禁用,避免界面重复提交相反结论。
48
64
  for (const item of actions.querySelectorAll('button'))
49
65
  item.disabled = true;
50
- const result = await run(() => api('POST', `/api/approval/tasks/${task.id}/${action}`, { comment: comment.value }), action === 'approve' ? '已通过审批' : '已驳回', button);
66
+ const result = await run(() => api('POST', `/api/approval/tasks/${task.id}/${action}`, { ...(management ? { reason: comment.value } : { comment: comment.value }), expectedRevision: task.revision }), { approve: '已通过审批', reject: '已驳回', claim: '已领取任务', unclaim: '已释放到候选池', resolve: '已归还原审批人' }[action], button);
51
67
  if (result)
52
68
  navigate('tasks');
53
69
  else
54
70
  for (const item of actions.querySelectorAll('button'))
55
71
  item.disabled = false;
56
72
  };
57
- actions.append(h('button', {
58
- class: 'small',
59
- onclick: (event) => submit('reject', event.currentTarget),
60
- }, '驳回'), h('button', {
61
- class: 'small primary',
62
- onclick: (event) => submit('approve', event.currentTarget),
63
- }, '通过审批'));
73
+ if (task.assigneeId === null || task.delegation === 'pending') {
74
+ const action = task.assigneeId === null ? 'claim' : 'resolve';
75
+ actions.append(h('button', { class: 'small primary', onclick: event => submit(action, event.currentTarget) }, action === 'claim' ? '领取任务' : '归还审批人'));
76
+ }
77
+ else {
78
+ if (task.assignment === 'candidate')
79
+ actions.append(h('button', { class: 'small', onclick: event => submit('unclaim', event.currentTarget) }, '释放任务'));
80
+ actions.append(h('button', {
81
+ class: 'small',
82
+ onclick: (event) => submit('reject', event.currentTarget),
83
+ }, '驳回'), h('button', {
84
+ class: 'small primary',
85
+ onclick: (event) => submit('approve', event.currentTarget),
86
+ }, '通过审批'));
87
+ }
64
88
  return h('div', { class: 'task-actions' }, comment, actions);
65
89
  },
66
90
  ],
package/ui/style.css CHANGED
@@ -225,6 +225,43 @@ pre {
225
225
  .muted {
226
226
  color: var(--muted);
227
227
  }
228
+ /* 审批提醒使用真实链接保存筛选 URL;选中态和键盘焦点都沿用系统绿色,状态不只靠文字颜色区分。 */
229
+ .approval-filter {
230
+ display: inline-flex;
231
+ align-items: center;
232
+ min-height: 30px;
233
+ padding: 4px 12px;
234
+ border: 1px solid var(--line-strong);
235
+ border-radius: 7px;
236
+ color: var(--ink);
237
+ text-decoration: none;
238
+ }
239
+ .approval-filter.selected {
240
+ background: var(--accent-soft);
241
+ border-color: var(--accent);
242
+ color: var(--accent-dark);
243
+ font-weight: 600;
244
+ }
245
+ .approval-filter:focus-visible {
246
+ outline: 2px solid var(--accent);
247
+ outline-offset: 2px;
248
+ }
249
+ /* 审批页支持窄侧面板:横向溢出仅留在表格内,避免整页滚动后操作按钮落到固定侧栏下。 */
250
+ body:has(.approval-workspace) { min-width: 0; }
251
+ .approval-workspace { min-width: 0; }
252
+ .approval-workspace .table-wrap { max-width: 100%; overflow-x: auto; }
253
+ .approval-workspace .table-toolbar { flex-wrap: wrap; gap: 12px; }
254
+ .approval-workspace table { min-width: 700px; }
255
+ @media (max-width: 700px) {
256
+ body:has(.approval-workspace) .app-sidebar { position: static; width: auto; padding: 16px; }
257
+ body:has(.approval-workspace) .app-sidebar nav { display: flex; flex-wrap: wrap; gap: 4px; }
258
+ body:has(.approval-workspace) .sidebar-bottom { display: none; }
259
+ body:has(.approval-workspace) .app-main { margin-left: 0; }
260
+ body:has(.approval-workspace) .app-topbar { padding: 0 16px; }
261
+ body:has(.approval-workspace) .page-container { padding: 22px 16px; }
262
+ body:has(.approval-workspace) .page-heading,
263
+ body:has(.approval-workspace) .page-footer { flex-wrap: wrap; }
264
+ }
228
265
  .eyebrow {
229
266
  display: block;
230
267
  margin-bottom: 4px;