feat: 完成分享、单会话与发布审批改造

- 增加工作流协作分享与知识库卡片分享入口,统一低版本浏览器复制反馈

- Web 新登录替换旧会话,并保持 API Key 会话隔离

- 发布审批增加必填说明并在审批详情展示

- 账号重置与导入改用可配置默认强密码
This commit is contained in:
2026-07-23 16:09:31 +08:00
parent caa1f07b66
commit 5a42826d44
71 changed files with 3191 additions and 132 deletions

View File

@@ -5,7 +5,7 @@ import { computed } from 'vue';
import { useAccess } from '@easyflow/access';
import { MoreFilled } from '@element-plus/icons-vue';
import { Loading, MoreFilled } from '@element-plus/icons-vue';
import {
ElAvatar,
ElButton,
@@ -27,6 +27,8 @@ export interface ActionButton {
icon?: any;
text: ((row: any) => string) | string;
className?: string;
disabled?: ((row: any) => boolean) | boolean;
loading?: ((row: any) => boolean) | boolean;
permission?: string;
placement?: ActionPlacement;
tone?: ActionTone;
@@ -87,6 +89,13 @@ function isActionVisible(action: ActionButton, row: any) {
return action.visible !== false;
}
function resolveActionState(
state: ((row: any) => boolean) | boolean | undefined,
row: any,
) {
return typeof state === 'function' ? state(row) : state === true;
}
const resolvedPrimaryAction = computed(() => {
if (!props.primaryAction || !hasPermission(props.primaryAction.permission)) {
return undefined;
@@ -125,6 +134,12 @@ function handlePrimaryAction(item: any) {
function handleActionClick(event: Event, action: ActionButton, item: any) {
event.stopPropagation();
if (
resolveActionState(action.disabled, item) ||
resolveActionState(action.loading, item)
) {
return;
}
action.onClick(item);
}
@@ -265,6 +280,8 @@ function resolveMetaItems(item: any) {
size="small"
class="card-action-btn"
:class="{ 'card-action-btn--danger': action.tone === 'danger' }"
:disabled="resolveActionState(action.disabled, item)"
:loading="resolveActionState(action.loading, item)"
link
@click.stop="handleActionClick($event, action, item)"
>
@@ -293,10 +310,20 @@ function resolveMetaItems(item: any) {
:class="{
'card-menu-item--danger': action.tone === 'danger',
}"
@click="action.onClick(item)"
:disabled="
resolveActionState(action.disabled, item) ||
resolveActionState(action.loading, item)
"
@click="handleActionClick($event, action, item)"
>
<div class="menu-action-content">
<ElIcon v-if="action.icon">
<ElIcon
v-if="resolveActionState(action.loading, item)"
class="is-loading"
>
<Loading />
</ElIcon>
<ElIcon v-else-if="action.icon">
<IconifyIcon
v-if="typeof action.icon === 'string'"
:icon="action.icon"

View File

@@ -89,6 +89,28 @@ describe('cardList', () => {
expect(primaryAction).not.toHaveBeenCalled();
});
it('异步次级操作执行中会禁用重复点击并展示加载态', async () => {
const inlineAction = vi.fn();
const wrapper = mountCardList({
actions: [
{
disabled: () => true,
loading: () => true,
text: '分享',
placement: 'inline',
onClick: inlineAction,
},
],
});
const actionButton = wrapper.get('.card-action-btn');
await actionButton.trigger('click');
expect(actionButton.classes()).toContain('is-loading');
expect(actionButton.classes()).toContain('is-disabled');
expect(inlineAction).not.toHaveBeenCalled();
});
it('键盘 Enter 可以触发主动作', async () => {
const primaryAction = vi.fn();
const wrapper = mountCardList({