feat: 增加工作流和知识库三级权限
- 抽取统一资源访问骨架与部门可见范围判断 - 接入工作流和知识库的 READ/MANAGE 权限校验 - 增加可见范围配置与只读态前端交互
This commit is contained in:
@@ -1,20 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import type {FormInstance} from 'element-plus';
|
||||
import {ElForm, ElFormItem, ElInput, ElInputNumber, ElMessage, ElMessageBox,} from 'element-plus';
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElPopover,
|
||||
} from 'element-plus';
|
||||
|
||||
import type {ActionButton, CardPrimaryAction,} from '#/components/page/CardList.vue';
|
||||
import CardList from '#/components/page/CardList.vue';
|
||||
|
||||
import {computed, markRaw, onMounted, ref} from 'vue';
|
||||
|
||||
import {useAccess} from '@easyflow/access';
|
||||
import {EasyFlowFormModal} from '@easyflow/common-ui';
|
||||
|
||||
import {
|
||||
Check,
|
||||
CopyDocument,
|
||||
Delete,
|
||||
Download,
|
||||
Edit,
|
||||
Lock,
|
||||
OfficeBuilding,
|
||||
Plus,
|
||||
Promotion,
|
||||
Tickets,
|
||||
Upload,
|
||||
VideoPlay,
|
||||
@@ -47,6 +61,8 @@ interface FieldDefinition {
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
type VisibilityScope = 'PRIVATE' | 'DEPT' | 'PUBLIC';
|
||||
|
||||
const primaryAction: CardPrimaryAction = {
|
||||
icon: DesignIcon,
|
||||
text: $t('button.design'),
|
||||
@@ -56,6 +72,39 @@ const primaryAction: CardPrimaryAction = {
|
||||
},
|
||||
};
|
||||
|
||||
const {hasAccessByCodes} = useAccess();
|
||||
const canManageWorkflow = computed(() =>
|
||||
hasAccessByCodes(['/api/v1/workflow/save']),
|
||||
);
|
||||
const updatingScopeId = ref<string | number | null>(null);
|
||||
const visibilityScopePopoverRefs = ref<Record<string, any>>({});
|
||||
const visibilityScopeMeta = computed(() => ({
|
||||
PRIVATE: {
|
||||
label: $t('aiWorkflow.visibilityScopePrivate'),
|
||||
description: $t('aiWorkflow.visibilityScopePrivateDesc'),
|
||||
icon: Lock,
|
||||
tone: 'private',
|
||||
},
|
||||
DEPT: {
|
||||
label: $t('aiWorkflow.visibilityScopeDept'),
|
||||
description: $t('aiWorkflow.visibilityScopeDeptDesc'),
|
||||
icon: OfficeBuilding,
|
||||
tone: 'dept',
|
||||
},
|
||||
PUBLIC: {
|
||||
label: $t('aiWorkflow.visibilityScopePublic'),
|
||||
description: $t('aiWorkflow.visibilityScopePublicDesc'),
|
||||
icon: Promotion,
|
||||
tone: 'public',
|
||||
},
|
||||
}));
|
||||
const visibilityScopeOptions = computed(() => {
|
||||
return (['PRIVATE', 'DEPT', 'PUBLIC'] as VisibilityScope[]).map((value) => ({
|
||||
value,
|
||||
...visibilityScopeMeta.value[value],
|
||||
}));
|
||||
});
|
||||
|
||||
const actions: ActionButton[] = [
|
||||
{
|
||||
icon: Edit,
|
||||
@@ -96,6 +145,7 @@ const actions: ActionButton[] = [
|
||||
{
|
||||
icon: Download,
|
||||
text: $t('button.export'),
|
||||
permission: '/api/v1/workflow/save',
|
||||
placement: 'menu',
|
||||
onClick: (row: any) => {
|
||||
exportJson(row);
|
||||
@@ -104,6 +154,7 @@ const actions: ActionButton[] = [
|
||||
{
|
||||
icon: CopyDocument,
|
||||
text: $t('button.copy'),
|
||||
permission: '/api/v1/workflow/save',
|
||||
placement: 'menu',
|
||||
onClick: (row: any) => {
|
||||
showDialog({
|
||||
@@ -151,6 +202,50 @@ const headerButtons = [
|
||||
function initDict() {
|
||||
dictStore.fetchDictionary('dataStatus');
|
||||
}
|
||||
function resolveVisibilityScopeMeta(scope?: string) {
|
||||
return visibilityScopeMeta.value[(scope || 'PRIVATE') as VisibilityScope] ||
|
||||
visibilityScopeMeta.value.PRIVATE;
|
||||
}
|
||||
function setVisibilityScopePopoverRef(id: string | number, el: any) {
|
||||
const cacheKey = String(id);
|
||||
if (el) {
|
||||
visibilityScopePopoverRefs.value[cacheKey] = el;
|
||||
return;
|
||||
}
|
||||
delete visibilityScopePopoverRefs.value[cacheKey];
|
||||
}
|
||||
function closeVisibilityScopePopover(id: string | number) {
|
||||
visibilityScopePopoverRefs.value[String(id)]?.hide?.();
|
||||
}
|
||||
async function updateVisibilityScope(
|
||||
row: any,
|
||||
visibilityScope: VisibilityScope,
|
||||
) {
|
||||
if (
|
||||
!canManageWorkflow.value ||
|
||||
!row?.id ||
|
||||
updatingScopeId.value === row.id ||
|
||||
row.visibilityScope === visibilityScope
|
||||
) {
|
||||
closeVisibilityScopePopover(row.id);
|
||||
return;
|
||||
}
|
||||
updatingScopeId.value = row.id;
|
||||
try {
|
||||
const res = await api.post('/api/v1/workflow/update', {
|
||||
id: row.id,
|
||||
visibilityScope,
|
||||
});
|
||||
if (res.errorCode === 0) {
|
||||
row.visibilityScope = visibilityScope;
|
||||
ElMessage.success(res.message);
|
||||
closeVisibilityScopePopover(row.id);
|
||||
pageDataRef.value?.reload?.();
|
||||
}
|
||||
} finally {
|
||||
updatingScopeId.value = null;
|
||||
}
|
||||
}
|
||||
const handleSearch = (params: string) => {
|
||||
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
|
||||
};
|
||||
@@ -341,7 +436,7 @@ function handleSubmit() {
|
||||
});
|
||||
}
|
||||
const getSideList = async () => {
|
||||
const [, res] = await tryit(api.get)('/api/v1/workflowCategory/list', {
|
||||
const [, res] = await tryit(api.get)('/api/v1/workflowCategory/visibleList', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
});
|
||||
|
||||
@@ -394,7 +489,90 @@ function handleHeaderButtonClick(data: any) {
|
||||
:data="pageList"
|
||||
:primary-action="primaryAction"
|
||||
:actions="actions"
|
||||
/>
|
||||
>
|
||||
<template #corner="{ item }">
|
||||
<ElPopover
|
||||
v-if="canManageWorkflow"
|
||||
:ref="(el) => setVisibilityScopePopoverRef(item.id, el)"
|
||||
trigger="click"
|
||||
placement="bottom-end"
|
||||
:width="208"
|
||||
popper-class="workflow-visibility-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<button
|
||||
type="button"
|
||||
class="workflow-scope-chip"
|
||||
:class="`workflow-scope-chip--${resolveVisibilityScopeMeta(item.visibilityScope).tone}`"
|
||||
:disabled="updatingScopeId === item.id"
|
||||
@click.stop
|
||||
>
|
||||
<ElIcon class="workflow-scope-chip__icon">
|
||||
<component
|
||||
:is="resolveVisibilityScopeMeta(item.visibilityScope).icon"
|
||||
/>
|
||||
</ElIcon>
|
||||
<span class="workflow-scope-chip__label">
|
||||
{{ resolveVisibilityScopeMeta(item.visibilityScope).label }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<div class="workflow-scope-panel" @click.stop>
|
||||
<button
|
||||
v-for="option in visibilityScopeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="workflow-scope-option"
|
||||
:class="[
|
||||
`workflow-scope-option--${option.tone}`,
|
||||
{
|
||||
'workflow-scope-option--active':
|
||||
item.visibilityScope === option.value,
|
||||
},
|
||||
]"
|
||||
:disabled="updatingScopeId === item.id"
|
||||
@click.stop="updateVisibilityScope(item, option.value)"
|
||||
>
|
||||
<span class="workflow-scope-option__leading">
|
||||
<span class="workflow-scope-option__icon-wrap">
|
||||
<ElIcon class="workflow-scope-option__icon">
|
||||
<component :is="option.icon" />
|
||||
</ElIcon>
|
||||
</span>
|
||||
<span class="workflow-scope-option__text">
|
||||
<span class="workflow-scope-option__label">
|
||||
{{ option.label }}
|
||||
</span>
|
||||
<span class="workflow-scope-option__desc">
|
||||
{{ option.description }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<ElIcon
|
||||
v-if="item.visibilityScope === option.value"
|
||||
class="workflow-scope-option__check"
|
||||
>
|
||||
<Check />
|
||||
</ElIcon>
|
||||
</button>
|
||||
</div>
|
||||
</ElPopover>
|
||||
<div
|
||||
v-else
|
||||
class="workflow-scope-chip workflow-scope-chip--readonly"
|
||||
:class="`workflow-scope-chip--${resolveVisibilityScopeMeta(item.visibilityScope).tone}`"
|
||||
>
|
||||
<ElIcon class="workflow-scope-chip__icon">
|
||||
<component
|
||||
:is="resolveVisibilityScopeMeta(item.visibilityScope).icon"
|
||||
/>
|
||||
</ElIcon>
|
||||
<span class="workflow-scope-chip__label">
|
||||
{{ resolveVisibilityScopeMeta(item.visibilityScope).label }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</CardList>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
@@ -440,4 +618,205 @@ function handleHeaderButtonClick(data: any) {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.workflow-scope-chip {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-height: 30px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: hsl(var(--text-strong));
|
||||
background: hsl(var(--surface-subtle) / 0.92);
|
||||
border: 1px solid hsl(var(--line-subtle));
|
||||
border-radius: 999px;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
background-color 0.18s ease,
|
||||
color 0.18s ease,
|
||||
transform 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
button.workflow-scope-chip {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.workflow-scope-chip:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 22px -18px hsl(var(--foreground) / 0.32);
|
||||
}
|
||||
|
||||
button.workflow-scope-chip:focus-visible {
|
||||
outline: none;
|
||||
box-shadow:
|
||||
0 0 0 4px hsl(var(--primary) / 0.12),
|
||||
0 10px 22px -18px hsl(var(--foreground) / 0.32);
|
||||
}
|
||||
|
||||
button.workflow-scope-chip:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.72;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.workflow-scope-chip--readonly {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.workflow-scope-chip__icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.workflow-scope-chip__label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.workflow-scope-chip--private {
|
||||
color: hsl(var(--primary));
|
||||
background: hsl(var(--primary) / 0.09);
|
||||
border-color: hsl(var(--primary) / 0.2);
|
||||
}
|
||||
|
||||
.workflow-scope-chip--dept {
|
||||
color: hsl(var(--warning));
|
||||
background: hsl(var(--warning) / 0.12);
|
||||
border-color: hsl(var(--warning) / 0.2);
|
||||
}
|
||||
|
||||
.workflow-scope-chip--public {
|
||||
color: hsl(var(--success));
|
||||
background: hsl(var(--success) / 0.12);
|
||||
border-color: hsl(var(--success) / 0.2);
|
||||
}
|
||||
|
||||
.workflow-scope-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.workflow-scope-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 10px 8px;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
transition:
|
||||
background-color 0.18s ease,
|
||||
transform 0.18s ease,
|
||||
color 0.18s ease;
|
||||
}
|
||||
|
||||
.workflow-scope-option:hover {
|
||||
background: hsl(var(--foreground) / 0.04);
|
||||
}
|
||||
|
||||
.workflow-scope-option:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 4px hsl(var(--primary) / 0.12);
|
||||
}
|
||||
|
||||
.workflow-scope-option:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.workflow-scope-option__leading {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.workflow-scope-option__icon-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 9px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.workflow-scope-option__icon {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.workflow-scope-option__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.workflow-scope-option__label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--text-strong));
|
||||
}
|
||||
|
||||
.workflow-scope-option__desc {
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.workflow-scope-option__check {
|
||||
flex-shrink: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.workflow-scope-option--private .workflow-scope-option__icon-wrap {
|
||||
color: hsl(var(--primary));
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
}
|
||||
|
||||
.workflow-scope-option--dept .workflow-scope-option__icon-wrap {
|
||||
color: hsl(var(--warning));
|
||||
background: hsl(var(--warning) / 0.12);
|
||||
}
|
||||
|
||||
.workflow-scope-option--public .workflow-scope-option__icon-wrap {
|
||||
color: hsl(var(--success));
|
||||
background: hsl(var(--success) / 0.12);
|
||||
}
|
||||
|
||||
.workflow-scope-option--private.workflow-scope-option--active {
|
||||
background: hsl(var(--primary) / 0.08);
|
||||
}
|
||||
|
||||
.workflow-scope-option--dept.workflow-scope-option--active {
|
||||
background: hsl(var(--warning) / 0.08);
|
||||
}
|
||||
|
||||
.workflow-scope-option--public.workflow-scope-option--active {
|
||||
background: hsl(var(--success) / 0.08);
|
||||
}
|
||||
|
||||
.workflow-scope-option--private .workflow-scope-option__check {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.workflow-scope-option--dept .workflow-scope-option__check {
|
||||
color: hsl(var(--warning));
|
||||
}
|
||||
|
||||
.workflow-scope-option--public .workflow-scope-option__check {
|
||||
color: hsl(var(--success));
|
||||
}
|
||||
|
||||
:global(.workflow-visibility-popover.el-popover.el-popper) {
|
||||
padding: 8px;
|
||||
border-radius: 16px;
|
||||
border-color: hsl(var(--line-subtle));
|
||||
box-shadow: 0 18px 34px -28px hsl(var(--foreground) / 0.2);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,15 @@ import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
|
||||
import { ElForm, ElFormItem, ElInput, ElMessage, ElUpload } from 'element-plus';
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElUpload,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
@@ -28,7 +36,7 @@ const isImport = ref(false);
|
||||
const jsonFile = ref<any>(null);
|
||||
const uploadFileList = ref<any[]>([]);
|
||||
const uploadRef = ref<UploadInstance>();
|
||||
const entity = ref<any>({
|
||||
const createDefaultEntity = () => ({
|
||||
alias: '',
|
||||
deptId: '',
|
||||
title: '',
|
||||
@@ -36,8 +44,24 @@ const entity = ref<any>({
|
||||
icon: '',
|
||||
content: '',
|
||||
englishName: '',
|
||||
visibilityScope: 'PRIVATE',
|
||||
});
|
||||
const entity = ref<any>(createDefaultEntity());
|
||||
const btnLoading = ref(false);
|
||||
const visibilityScopeOptions = computed(() => [
|
||||
{
|
||||
label: $t('aiWorkflow.visibilityScopePrivate'),
|
||||
value: 'PRIVATE',
|
||||
},
|
||||
{
|
||||
label: $t('aiWorkflow.visibilityScopeDept'),
|
||||
value: 'DEPT',
|
||||
},
|
||||
{
|
||||
label: $t('aiWorkflow.visibilityScopePublic'),
|
||||
value: 'PUBLIC',
|
||||
},
|
||||
]);
|
||||
const jsonFileModel = computed({
|
||||
get: () => (uploadFileList.value.length > 0 ? uploadFileList.value[0] : null),
|
||||
set: (value: any) => {
|
||||
@@ -57,10 +81,11 @@ const rules = computed(() => ({
|
||||
// functions
|
||||
function openDialog(row: any, importMode = false) {
|
||||
isImport.value = importMode;
|
||||
if (row.id) {
|
||||
isAdd.value = false;
|
||||
}
|
||||
entity.value = row;
|
||||
isAdd.value = !row?.id;
|
||||
entity.value = {
|
||||
...createDefaultEntity(),
|
||||
...(row || {}),
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
@@ -137,7 +162,7 @@ function closeDialog() {
|
||||
jsonFile.value = null;
|
||||
isAdd.value = true;
|
||||
isImport.value = false;
|
||||
entity.value = {};
|
||||
entity.value = createDefaultEntity();
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
</script>
|
||||
@@ -198,6 +223,19 @@ function closeDialog() {
|
||||
dict-code="aiWorkFlowCategory"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="visibilityScope"
|
||||
:label="$t('aiWorkflow.visibilityScope')"
|
||||
>
|
||||
<ElSelect v-model="entity.visibilityScope" class="w-full">
|
||||
<ElOption
|
||||
v-for="item in visibilityScopeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="alias" :label="$t('aiWorkflow.alias')">
|
||||
<ElInput v-model.trim="entity.alias" />
|
||||
</ElFormItem>
|
||||
|
||||
Reference in New Issue
Block a user