feat(feedback): 增加反馈搜索状态筛选功能
- 在反馈搜索组件中引入状态字典选项 - 为反馈搜索区域增加状态选择器字段 - 设置反馈页面初始搜索参数中的状态值为1 fix(personal-center): 限制已完成项目的编辑操作 - 根据项目完成状态动态控制编辑按钮的显示 - 从个人中心我的项目详情对话框中移除已完成项目的操作权限 refactor(project): 优化任务操作权限控制逻辑 - 移除原有的任务权限钩子函数依赖 - 使用当前用户身份直接判断任务操作权限 - 只有任务负责人才能进行编辑和删除操作 - 已完成任务不可编辑 - 重构成更清晰的任务操作按钮显示逻辑 fix(project): 完成任务禁用工时记录删除功能 - 添加计算属性控制完成状态下的工时记录删除权限 - 在工时记录面板中根据任务状态控制删除操作的显示
This commit is contained in:
@@ -45,7 +45,7 @@ function canEditRow(row: Api.Feedback.FeedbackItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getInitSearchParams(): Api.Feedback.FeedbackSearchParams {
|
function getInitSearchParams(): Api.Feedback.FeedbackSearchParams {
|
||||||
return { pageNo: 1, pageSize: 20, type: undefined, status: undefined, title: undefined, creator: undefined };
|
return { pageNo: 1, pageSize: 20, type: undefined, status: 1, title: undefined, creator: undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformPageResult(
|
function transformPageResult(
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { FEEDBACK_STATUS_DICT_CODE } from '@/constants/dict';
|
||||||
import { fetchGetUserSimpleList } from '@/service/api';
|
import { fetchGetUserSimpleList } from '@/service/api';
|
||||||
|
import { useDict } from '@/hooks/business/dict';
|
||||||
import TableSearchFields, { type SearchField } from '@/components/custom/table-search-fields.vue';
|
import TableSearchFields, { type SearchField } from '@/components/custom/table-search-fields.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'FeedbackSearch' });
|
defineOptions({ name: 'FeedbackSearch' });
|
||||||
@@ -14,8 +16,9 @@ const model = defineModel<Api.Feedback.FeedbackSearchParams>('model', { required
|
|||||||
|
|
||||||
// 提交人下拉选项(全员简易列表;value 取用户 id,ID 铁律 string)
|
// 提交人下拉选项(全员简易列表;value 取用户 id,ID 铁律 string)
|
||||||
const userOptions = ref<{ label: string; value: string }[]>([]);
|
const userOptions = ref<{ label: string; value: string }[]>([]);
|
||||||
|
const { dictOptions: statusOptions } = useDict(FEEDBACK_STATUS_DICT_CODE);
|
||||||
|
|
||||||
// 分类 / 状态筛选已迁移至左侧分面面板,搜索区保留标题关键词 + 提交人
|
// 分类保留在左侧分面;搜索区补充标题关键词、提交人、状态
|
||||||
const fields = computed<SearchField[]>(() => [
|
const fields = computed<SearchField[]>(() => [
|
||||||
{ key: 'title', label: '标题', type: 'input', placeholder: '请输入标题关键词' },
|
{ key: 'title', label: '标题', type: 'input', placeholder: '请输入标题关键词' },
|
||||||
{
|
{
|
||||||
@@ -25,6 +28,13 @@ const fields = computed<SearchField[]>(() => [
|
|||||||
placeholder: '请选择提交人',
|
placeholder: '请选择提交人',
|
||||||
filterable: true,
|
filterable: true,
|
||||||
options: userOptions.value
|
options: userOptions.value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
label: '状态',
|
||||||
|
type: 'select',
|
||||||
|
placeholder: '请选择状态',
|
||||||
|
options: statusOptions.value
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -281,17 +281,21 @@ function buildRowActions(row: Api.PersonalItem.PersonalItem): PersonalItemRowAct
|
|||||||
type: 'primary',
|
type: 'primary',
|
||||||
onClick: async () => openDetail(row, 'worklog')
|
onClick: async () => openDetail(row, 'worklog')
|
||||||
},
|
},
|
||||||
{
|
...(!isCompleted
|
||||||
key: 'edit',
|
? ([
|
||||||
tooltip: '编辑',
|
{
|
||||||
icon: markRaw(IconMdiPencilOutline),
|
key: 'edit',
|
||||||
type: 'primary',
|
tooltip: '编辑',
|
||||||
onClick: async () => {
|
icon: markRaw(IconMdiPencilOutline),
|
||||||
operateType.value = 'edit';
|
type: 'primary',
|
||||||
editingData.value = row;
|
onClick: async () => {
|
||||||
openOperateDialog();
|
operateType.value = 'edit';
|
||||||
}
|
editingData.value = row;
|
||||||
},
|
openOperateDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
] satisfies PersonalItemRowAction[])
|
||||||
|
: []),
|
||||||
...(!isCompleted
|
...(!isCompleted
|
||||||
? ([
|
? ([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -81,10 +81,7 @@ const totalHoursText = computed(() => {
|
|||||||
});
|
});
|
||||||
const canSubmitWorklog = computed(() =>
|
const canSubmitWorklog = computed(() =>
|
||||||
Boolean(
|
Boolean(
|
||||||
detailData.value?.id &&
|
detailData.value?.id && (detailData.value.statusCode === 'pending' || detailData.value.statusCode === 'active')
|
||||||
(detailData.value.statusCode === 'pending' ||
|
|
||||||
detailData.value.statusCode === 'active' ||
|
|
||||||
detailData.value.statusCode === 'completed')
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import { markRaw } from 'vue';
|
import { computed, markRaw } from 'vue';
|
||||||
import { useTaskPermissions } from './use-task-permissions';
|
import { useAuthStore } from '@/store/modules/auth';
|
||||||
import IconMdiCheckCircleOutline from '~icons/mdi/check-circle-outline';
|
|
||||||
import IconMdiClipboardEditOutline from '~icons/mdi/clipboard-edit-outline';
|
import IconMdiClipboardEditOutline from '~icons/mdi/clipboard-edit-outline';
|
||||||
import IconMdiCloseCircleOutline from '~icons/mdi/close-circle-outline';
|
|
||||||
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
||||||
import IconMdiPause from '~icons/mdi/pause';
|
|
||||||
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
||||||
import IconMdiRestart from '~icons/mdi/restart';
|
|
||||||
import IconMdiSync from '~icons/mdi/sync';
|
|
||||||
|
|
||||||
export interface TaskAction {
|
export interface TaskAction {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -27,43 +22,22 @@ export interface TaskActionEmits {
|
|||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_ACTION_ICON_MAP: Record<string, object> = {
|
|
||||||
pause: markRaw(IconMdiPause),
|
|
||||||
complete: markRaw(IconMdiCheckCircleOutline),
|
|
||||||
resume: markRaw(IconMdiRestart),
|
|
||||||
cancel: markRaw(IconMdiCloseCircleOutline)
|
|
||||||
};
|
|
||||||
|
|
||||||
// 状态推进按钮 type 映射(对齐执行 execution-section.vue 同源语义):
|
|
||||||
// cancel 破坏性=红,pause 中断=橙,complete 完结=绿,resume 主动作=蓝
|
|
||||||
const STATUS_ACTION_TYPE_MAP: Record<string, TaskAction['type']> = {
|
|
||||||
cancel: 'danger',
|
|
||||||
pause: 'warning',
|
|
||||||
complete: 'success',
|
|
||||||
resume: 'primary'
|
|
||||||
};
|
|
||||||
|
|
||||||
// 同一状态下多个推进按钮的展示顺序,与执行侧保持一致:暂停 → 取消 → 完成 → 恢复
|
|
||||||
const STATUS_ACTION_ORDER: Record<string, number> = {
|
|
||||||
pause: 1,
|
|
||||||
cancel: 2,
|
|
||||||
complete: 3,
|
|
||||||
resume: 4
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 任务行操作按钮的集中装配。
|
* 任务行操作按钮的集中装配。
|
||||||
*
|
*
|
||||||
* 表格操作列与看板卡片操作区共用同一份语义:填报 / 编辑 / 删除 / 状态推进按钮,
|
* 只在这里收口任务行按钮的最终显示口径,不改动底层子任务 / 父任务负责人 / 执行负责人那套权限体系:
|
||||||
* 含 auto_start 过滤、complete 进度 100% 兜底、按钮排序与 icon/type 映射。
|
* - 当前用户是任务负责人(task.ownerId): 显示 填报 / 编辑 / 删除
|
||||||
|
* - 当前用户是协办人(task.assignees[].userId): 只显示 填报
|
||||||
|
* - 都不是: 也显示 填报
|
||||||
*/
|
*/
|
||||||
export function useTaskActions(emits: TaskActionEmits) {
|
export function useTaskActions(emits: TaskActionEmits) {
|
||||||
const { canEditTask, canDeleteTask } = useTaskPermissions();
|
const authStore = useAuthStore();
|
||||||
|
const currentUserId = computed(() => authStore.userInfo.userId || '');
|
||||||
|
|
||||||
function createActions(row: Api.Project.ProjectTask): TaskAction[] {
|
function createActions(row: Api.Project.ProjectTask): TaskAction[] {
|
||||||
const actions: TaskAction[] = [];
|
const actions: TaskAction[] = [];
|
||||||
|
const isOwner = Boolean(currentUserId.value) && row.ownerId === currentUserId.value;
|
||||||
|
|
||||||
// 填报:行操作入口始终显示——查看人人可看;新增/编辑由弹层内 canSubmit 按身份与状态控制
|
|
||||||
actions.push({
|
actions.push({
|
||||||
key: 'report',
|
key: 'report',
|
||||||
tooltip: '填报',
|
tooltip: '填报',
|
||||||
@@ -72,7 +46,11 @@ export function useTaskActions(emits: TaskActionEmits) {
|
|||||||
onClick: () => emits.report(row)
|
onClick: () => emits.report(row)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (canEditTask(row)) {
|
if (!isOwner) {
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.statusCode !== 'completed') {
|
||||||
actions.push({
|
actions.push({
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
tooltip: '编辑',
|
tooltip: '编辑',
|
||||||
@@ -82,41 +60,12 @@ export function useTaskActions(emits: TaskActionEmits) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canDeleteTask(row)) {
|
actions.push({
|
||||||
actions.push({
|
key: 'delete',
|
||||||
key: 'delete',
|
tooltip: '删除',
|
||||||
tooltip: '删除',
|
icon: markRaw(IconMdiDeleteOutline),
|
||||||
icon: markRaw(IconMdiDeleteOutline),
|
type: 'danger',
|
||||||
type: 'danger',
|
onClick: () => emits.remove(row)
|
||||||
onClick: () => emits.remove(row)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!row.availableActions.length) {
|
|
||||||
return actions;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 排序:暂停 → 取消 → 完成 → 恢复(对齐执行)
|
|
||||||
const sortedActions = [...row.availableActions].sort(
|
|
||||||
(a, b) => (STATUS_ACTION_ORDER[a.actionCode] ?? 99) - (STATUS_ACTION_ORDER[b.actionCode] ?? 99)
|
|
||||||
);
|
|
||||||
|
|
||||||
sortedActions.forEach(action => {
|
|
||||||
// auto_start 由后端在填工时时自动触发,无手工开始通道;防御后端误下发,前端不渲染
|
|
||||||
if (action.actionCode === 'auto_start') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 完成任务至少要求任务进度达到 100%;父级提交入口仍保留同样兜底校验
|
|
||||||
if (action.actionCode === 'complete' && row.progressRate < 100) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
actions.push({
|
|
||||||
key: `status-${action.actionCode}`,
|
|
||||||
tooltip: action.actionName,
|
|
||||||
icon: markRaw(STATUS_ACTION_ICON_MAP[action.actionCode] ?? IconMdiSync),
|
|
||||||
type: STATUS_ACTION_TYPE_MAP[action.actionCode] ?? 'primary',
|
|
||||||
onClick: () => emits.statusAction(row, action)
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return actions;
|
return actions;
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ const canCreate = computed(() => Boolean(props.canSubmit && props.taskId));
|
|||||||
const isWorklogMutableStatus = computed(
|
const isWorklogMutableStatus = computed(
|
||||||
() => props.taskStatusCode === 'active' || props.taskStatusCode === 'completed'
|
() => props.taskStatusCode === 'active' || props.taskStatusCode === 'completed'
|
||||||
);
|
);
|
||||||
|
const showDeleteAction = computed(() => props.taskStatusCode !== 'completed');
|
||||||
|
|
||||||
function canEditRow(row: Api.Project.TaskWorklog) {
|
function canEditRow(row: Api.Project.TaskWorklog) {
|
||||||
return Boolean(isWorklogMutableStatus.value && currentUserId.value && row.userId === currentUserId.value);
|
return Boolean(isWorklogMutableStatus.value && currentUserId.value && row.userId === currentUserId.value);
|
||||||
@@ -706,31 +707,33 @@ watch(
|
|||||||
</ElButton>
|
</ElButton>
|
||||||
</span>
|
</span>
|
||||||
</ElTooltip>
|
</ElTooltip>
|
||||||
<ElPopconfirm
|
<template v-if="showDeleteAction">
|
||||||
v-if="canDeleteRow(row)"
|
<ElPopconfirm
|
||||||
title="确认删除该条工时记录?"
|
v-if="canDeleteRow(row)"
|
||||||
confirm-button-text="删除"
|
title="确认删除该条工时记录?"
|
||||||
cancel-button-text="取消"
|
confirm-button-text="删除"
|
||||||
confirm-button-type="danger"
|
cancel-button-text="取消"
|
||||||
@confirm="handleDelete(row)"
|
confirm-button-type="danger"
|
||||||
>
|
@confirm="handleDelete(row)"
|
||||||
<template #reference>
|
>
|
||||||
|
<template #reference>
|
||||||
|
<span class="inline-flex">
|
||||||
|
<ElTooltip content="删除">
|
||||||
|
<ElButton link type="danger" class="task-worklog-panel__action-btn">
|
||||||
|
<IconMdiDeleteOutline class="text-15px" />
|
||||||
|
</ElButton>
|
||||||
|
</ElTooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</ElPopconfirm>
|
||||||
|
<ElTooltip v-else content="仅可删除本人填报">
|
||||||
<span class="inline-flex">
|
<span class="inline-flex">
|
||||||
<ElTooltip content="删除">
|
<ElButton link type="danger" class="task-worklog-panel__action-btn" disabled>
|
||||||
<ElButton link type="danger" class="task-worklog-panel__action-btn">
|
<IconMdiDeleteOutline class="text-15px" />
|
||||||
<IconMdiDeleteOutline class="text-15px" />
|
</ElButton>
|
||||||
</ElButton>
|
|
||||||
</ElTooltip>
|
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</ElTooltip>
|
||||||
</ElPopconfirm>
|
</template>
|
||||||
<ElTooltip v-else content="仅可删除本人填报">
|
|
||||||
<span class="inline-flex">
|
|
||||||
<ElButton link type="danger" class="task-worklog-panel__action-btn" disabled>
|
|
||||||
<IconMdiDeleteOutline class="text-15px" />
|
|
||||||
</ElButton>
|
|
||||||
</span>
|
|
||||||
</ElTooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
|
|||||||
Reference in New Issue
Block a user