feat(projects): 新增项目、执行、任务等功能
This commit is contained in:
@@ -247,7 +247,12 @@ watch([() => visible.value, () => props.productId], ([currentVisible, productId]
|
||||
</div>
|
||||
|
||||
<p class="product-activity-dialog__sentence">
|
||||
<span class="product-activity-dialog__sentence-main">{{ item.compactText }}</span>
|
||||
<span class="product-activity-dialog__sentence-main">
|
||||
<template v-for="(part, index) in item.compactTextParts" :key="`${item.id}-${index}`">
|
||||
<strong v-if="part.strong" class="product-activity-dialog__subject">{{ part.text }}</strong>
|
||||
<span v-else>{{ part.text }}</span>
|
||||
</template>
|
||||
</span>
|
||||
<span v-if="item.statusTransition">,状态:{{ item.statusTransition }}</span>
|
||||
<span v-if="item.reasonText">,原因:{{ item.reasonText }}</span>
|
||||
</p>
|
||||
@@ -497,6 +502,11 @@ watch([() => visible.value, () => props.productId], ([currentVisible, productId]
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.product-activity-dialog__subject {
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.product-activity-dialog__footer-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -112,7 +112,12 @@ watch(
|
||||
</div>
|
||||
|
||||
<p class="product-activity-panel__sentence">
|
||||
<span class="product-activity-panel__sentence-main">{{ item.compactText }}</span>
|
||||
<span class="product-activity-panel__sentence-main">
|
||||
<template v-for="(part, index) in item.compactTextParts" :key="`${item.id}-${index}`">
|
||||
<strong v-if="part.strong" class="product-activity-panel__subject">{{ part.text }}</strong>
|
||||
<span v-else>{{ part.text }}</span>
|
||||
</template>
|
||||
</span>
|
||||
<span v-if="item.statusTransition">,状态:{{ item.statusTransition }}</span>
|
||||
<span v-if="item.reasonText">,原因:{{ item.reasonText }}</span>
|
||||
</p>
|
||||
@@ -262,6 +267,11 @@ watch(
|
||||
color: rgb(15 23 42 / 98%);
|
||||
}
|
||||
|
||||
.product-activity-panel__subject {
|
||||
color: rgb(15 23 42 / 98%);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.product-activity-panel__body {
|
||||
min-height: auto;
|
||||
|
||||
@@ -17,13 +17,20 @@ export type ProductActivityFilterType = 'all' | Api.Product.ProductActivityType;
|
||||
|
||||
export type ProductActivityTone = 'sky' | 'emerald' | 'amber' | 'rose' | 'slate';
|
||||
|
||||
export interface ProductActivityTextPart {
|
||||
text: string;
|
||||
strong?: boolean;
|
||||
}
|
||||
|
||||
export interface ProductActivityDisplayItem extends Api.Product.ProductActivityTimelineItem {
|
||||
tagLabel: string;
|
||||
timeText: string;
|
||||
actionText: string;
|
||||
displaySummary: string;
|
||||
compactText: string;
|
||||
compactTextParts: ProductActivityTextPart[];
|
||||
operatorText: string;
|
||||
subjectText: string;
|
||||
reasonText: string;
|
||||
statusTransition: string;
|
||||
tone: ProductActivityTone;
|
||||
@@ -250,6 +257,10 @@ function isGenericActivitySummary(summaryText: string, actionText: string) {
|
||||
return summaryText === actionText || summaryText === actionText.replace('执行了', '执行了');
|
||||
}
|
||||
|
||||
function isMemberActivityAction(actionType: Api.Product.ProductActivityActionType) {
|
||||
return actionType === 'add_member' || actionType === 'remove_member' || actionType === 'update_member';
|
||||
}
|
||||
|
||||
function buildMemberChangeSummary(
|
||||
item: Api.Product.ProductActivityTimelineItem,
|
||||
detailsRecord: ActivityDetailRecord | null,
|
||||
@@ -263,9 +274,10 @@ function buildMemberChangeSummary(
|
||||
}
|
||||
|
||||
const memberDetail = roleName ? `${memberName}(${roleName})` : memberName;
|
||||
const actionLabel = item.actionType === 'add_member' ? '将成员加入产品' : '将成员移出产品';
|
||||
|
||||
return operatorText === '--' ? `${actionLabel}:${memberDetail}` : `${operatorText}${actionLabel}:${memberDetail}`;
|
||||
return operatorText === '--'
|
||||
? `执行了【${item.actionName}】:${memberDetail}`
|
||||
: `${operatorText}执行了【${item.actionName}】:${memberDetail}`;
|
||||
}
|
||||
|
||||
function buildMemberUpdateSummary(
|
||||
@@ -279,8 +291,8 @@ function buildMemberUpdateSummary(
|
||||
const roleText = roleTransitionText ? `,角色:${roleTransitionText}` : '';
|
||||
|
||||
return operatorText === '--'
|
||||
? `调整成员:${memberText}${roleText}`
|
||||
: `${operatorText}调整成员:${memberText}${roleText}`;
|
||||
? `执行了【${item.actionName}】:${memberText}${roleText}`
|
||||
: `${operatorText}执行了【${item.actionName}】:${memberText}${roleText}`;
|
||||
}
|
||||
|
||||
function buildManagerChangeSummary(detailsRecord: ActivityDetailRecord | null, operatorText: string) {
|
||||
@@ -309,15 +321,11 @@ function buildManagerChangeSummary(detailsRecord: ActivityDetailRecord | null, o
|
||||
|
||||
function resolveDetailedSummary(
|
||||
item: Api.Product.ProductActivityTimelineItem,
|
||||
operatorText: string,
|
||||
actionText: string
|
||||
detailsRecord: ActivityDetailRecord | null,
|
||||
texts: { operatorText: string; actionText: string }
|
||||
) {
|
||||
const { operatorText, actionText } = texts;
|
||||
const summaryText = item.summary?.trim() || '';
|
||||
const detailsRecord = parseActivityDetails(item.details);
|
||||
|
||||
if (!isGenericActivitySummary(summaryText, actionText)) {
|
||||
return summaryText;
|
||||
}
|
||||
|
||||
if (item.actionType === 'add_member' || item.actionType === 'remove_member') {
|
||||
return buildMemberChangeSummary(item, detailsRecord, operatorText) || summaryText || actionText;
|
||||
@@ -327,6 +335,10 @@ function resolveDetailedSummary(
|
||||
return buildMemberUpdateSummary(item, detailsRecord, operatorText);
|
||||
}
|
||||
|
||||
if (!isGenericActivitySummary(summaryText, actionText)) {
|
||||
return summaryText;
|
||||
}
|
||||
|
||||
if (item.actionType === 'change_manager') {
|
||||
return buildManagerChangeSummary(detailsRecord, operatorText) || summaryText || actionText;
|
||||
}
|
||||
@@ -334,13 +346,31 @@ function resolveDetailedSummary(
|
||||
return summaryText || actionText;
|
||||
}
|
||||
|
||||
function buildProductActivityTextParts(text: string, subjectText: string): ProductActivityTextPart[] {
|
||||
const normalizedSubject = subjectText.trim();
|
||||
const subjectIndex = normalizedSubject ? text.indexOf(normalizedSubject) : -1;
|
||||
|
||||
if (subjectIndex < 0) {
|
||||
return [{ text }];
|
||||
}
|
||||
|
||||
return [
|
||||
{ text: text.slice(0, subjectIndex) },
|
||||
{ text: normalizedSubject, strong: true },
|
||||
{ text: text.slice(subjectIndex + normalizedSubject.length) }
|
||||
].filter(part => part.text);
|
||||
}
|
||||
|
||||
export function buildProductActivityDisplayItem(
|
||||
item: Api.Product.ProductActivityTimelineItem
|
||||
): ProductActivityDisplayItem {
|
||||
const operatorText = item.operatorName?.trim() || '--';
|
||||
const actionText =
|
||||
operatorText === '--' ? `执行了【${item.actionName}】` : `${operatorText}执行了【${item.actionName}】`;
|
||||
const displaySummary = item.type === 'status' ? actionText : resolveDetailedSummary(item, operatorText, actionText);
|
||||
const detailsRecord = parseActivityDetails(item.details);
|
||||
const subjectText = isMemberActivityAction(item.actionType) ? getActivityTargetUserName(item, detailsRecord) : '';
|
||||
const displaySummary =
|
||||
item.type === 'status' ? actionText : resolveDetailedSummary(item, detailsRecord, { operatorText, actionText });
|
||||
const compactText = displaySummary;
|
||||
|
||||
return {
|
||||
@@ -350,7 +380,9 @@ export function buildProductActivityDisplayItem(
|
||||
actionText,
|
||||
displaySummary,
|
||||
compactText,
|
||||
compactTextParts: buildProductActivityTextParts(compactText, subjectText),
|
||||
operatorText,
|
||||
subjectText,
|
||||
reasonText: item.reason?.trim() || '',
|
||||
statusTransition:
|
||||
item.type === 'status' && item.fromStatus && item.toStatus
|
||||
|
||||
@@ -6,7 +6,7 @@ import dayjs from 'dayjs';
|
||||
import { CircleCheckFilled, DeleteFilled, FolderOpened, VideoPause } from '@element-plus/icons-vue';
|
||||
import { RDMS_OBJECT_DIRECTION_DICT_CODE } from '@/constants/dict';
|
||||
import { OBJECT_CONTEXT_QUERY_KEY } from '@/constants/object-context';
|
||||
import { fetchGetProductPage, fetchGetUserSimpleList } from '@/service/api';
|
||||
import { fetchGetProductOverviewSummary, fetchGetProductPage, fetchGetUserSimpleList } from '@/service/api';
|
||||
import { useDict } from '@/hooks/business/dict';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||
@@ -27,7 +27,6 @@ interface StatusNavMeta {
|
||||
|
||||
type ProductPageResponse = Awaited<ReturnType<typeof fetchGetProductPage>>;
|
||||
|
||||
const PRODUCT_OPTION_PAGE_SIZE = 200;
|
||||
const PRODUCT_ENTRY_ROUTE_PATH = '/product/list';
|
||||
|
||||
function getInitSearchParams(): Api.Product.ProductSearchParams {
|
||||
@@ -72,59 +71,6 @@ function formatDateTime(value?: string | null) {
|
||||
return dayjs(value).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
async function fetchProductTotal(params: Api.Product.ProductSearchParams) {
|
||||
const { error, data } = await fetchGetProductPage({
|
||||
...params,
|
||||
pageNo: 1,
|
||||
pageSize: 1
|
||||
});
|
||||
|
||||
if (error || !data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return data.total;
|
||||
}
|
||||
|
||||
async function fetchAllProducts() {
|
||||
async function collect(pageNo: number, list: Api.Product.Product[]): Promise<Api.Product.Product[] | null> {
|
||||
const { error, data } = await fetchGetProductPage({
|
||||
pageNo,
|
||||
pageSize: PRODUCT_OPTION_PAGE_SIZE
|
||||
});
|
||||
|
||||
if (error || !data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextList = list.concat(data.list);
|
||||
|
||||
if (nextList.length >= data.total || data.list.length === 0) {
|
||||
return nextList;
|
||||
}
|
||||
|
||||
return collect(pageNo + 1, nextList);
|
||||
}
|
||||
|
||||
return collect(1, []);
|
||||
}
|
||||
|
||||
function createManagerOptions(products: Api.Product.Product[], users: Api.SystemManage.UserSimple[]) {
|
||||
const managerIdSet = new Set(products.map(item => String(item.managerUserId)).filter(Boolean));
|
||||
const userMap = new Map(users.map(item => [String(item.id), item]));
|
||||
|
||||
const options = Array.from(managerIdSet).map(managerUserId => {
|
||||
return (
|
||||
userMap.get(managerUserId) || {
|
||||
id: managerUserId,
|
||||
nickname: String(managerUserId)
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return sortManagerOptions(options);
|
||||
}
|
||||
|
||||
const statusNavMetas: StatusNavMeta[] = [
|
||||
{
|
||||
key: 'active',
|
||||
@@ -166,15 +112,13 @@ const { routerPush } = useRouterPush();
|
||||
|
||||
const { dictData: directionOptions, getLabel: getDirectionDictLabel } = useDict(RDMS_OBJECT_DIRECTION_DICT_CODE);
|
||||
|
||||
const statusCounts = ref<Record<Api.Product.ProductStatusCode, number>>({
|
||||
const statusCounts = ref<Record<string, number>>({
|
||||
active: 0,
|
||||
archived: 0,
|
||||
paused: 0,
|
||||
abandoned: 0
|
||||
});
|
||||
|
||||
const recentUpdatedCount = ref(0);
|
||||
|
||||
const managerLabelMap = computed(() => {
|
||||
return new Map(managerUserOptions.value.map(item => [String(item.id), item.nickname]));
|
||||
});
|
||||
@@ -182,7 +126,7 @@ const managerLabelMap = computed(() => {
|
||||
const statusItems = computed(() =>
|
||||
statusNavMetas.map(item => ({
|
||||
...item,
|
||||
count: statusCounts.value[item.key]
|
||||
count: statusCounts.value[item.key] ?? 0
|
||||
}))
|
||||
);
|
||||
|
||||
@@ -194,7 +138,7 @@ const overviewMetrics = computed(() => [
|
||||
},
|
||||
{
|
||||
label: '当前启用',
|
||||
value: statusCounts.value.active,
|
||||
value: statusCounts.value.active ?? 0,
|
||||
hint: '正在持续服务和维护的产品'
|
||||
},
|
||||
{
|
||||
@@ -203,9 +147,9 @@ const overviewMetrics = computed(() => [
|
||||
hint: '已加载的方向字典项数量'
|
||||
},
|
||||
{
|
||||
label: '30天内更新',
|
||||
value: recentUpdatedCount.value,
|
||||
hint: '最近 30 天内发生过更新的产品'
|
||||
label: '废弃产品',
|
||||
value: statusCounts.value.abandoned ?? 0,
|
||||
hint: '已明确停止建设的产品'
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -312,44 +256,33 @@ const { columns, columnChecks, data, loading, getDataByPage, mobilePagination }
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
],
|
||||
immediate: false
|
||||
});
|
||||
|
||||
async function loadManagerOptions() {
|
||||
const [allProducts, userSimpleResult] = await Promise.all([fetchAllProducts(), fetchGetUserSimpleList()]);
|
||||
const { error, data: userList } = await fetchGetUserSimpleList();
|
||||
|
||||
const userSimpleList =
|
||||
userSimpleResult.error || !userSimpleResult.data ? [] : sortManagerOptions(userSimpleResult.data);
|
||||
|
||||
managerUserOptions.value = userSimpleList;
|
||||
|
||||
if (!allProducts) {
|
||||
if (error || !userList) {
|
||||
managerUserOptions.value = [];
|
||||
managerFilterOptions.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
managerFilterOptions.value = createManagerOptions(allProducts, userSimpleList);
|
||||
const userSimpleList = sortManagerOptions(userList);
|
||||
managerUserOptions.value = userSimpleList;
|
||||
managerFilterOptions.value = userSimpleList;
|
||||
}
|
||||
|
||||
async function loadOverviewData() {
|
||||
const end = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
|
||||
const start = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss');
|
||||
const { error, data: overviewSummary } = await fetchGetProductOverviewSummary();
|
||||
|
||||
const [activeTotal, archivedTotal, pausedTotal, abandonedTotal, recentTotal] = await Promise.all([
|
||||
fetchProductTotal({ pageNo: 1, pageSize: 1, statusCode: 'active' }),
|
||||
fetchProductTotal({ pageNo: 1, pageSize: 1, statusCode: 'archived' }),
|
||||
fetchProductTotal({ pageNo: 1, pageSize: 1, statusCode: 'paused' }),
|
||||
fetchProductTotal({ pageNo: 1, pageSize: 1, statusCode: 'abandoned' }),
|
||||
fetchProductTotal({ pageNo: 1, pageSize: 1, updateTime: [start, end] })
|
||||
]);
|
||||
if (error || !overviewSummary) {
|
||||
statusCounts.value = {};
|
||||
return;
|
||||
}
|
||||
|
||||
statusCounts.value = {
|
||||
active: activeTotal,
|
||||
archived: archivedTotal,
|
||||
paused: pausedTotal,
|
||||
abandoned: abandonedTotal
|
||||
};
|
||||
recentUpdatedCount.value = recentTotal;
|
||||
statusCounts.value = overviewSummary.statusCounts || {};
|
||||
}
|
||||
|
||||
async function reloadProductTable(page = searchParams.pageNo ?? 1) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { RDMS_OBJECT_DIRECTION_DICT_CODE } from '@/constants/dict';
|
||||
import { fetchCreateProduct, fetchGetProduct, fetchUpdateProduct } from '@/service/api';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import BusinessUserSelect from '@/components/custom/business-user-select.vue';
|
||||
import DictSelect from '@/components/custom/dict-select.vue';
|
||||
|
||||
defineOptions({ name: 'ProductOperateDialog' });
|
||||
@@ -166,14 +167,14 @@ watch(visible, async value => {
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
:title="dialogTitle"
|
||||
preset="lg"
|
||||
preset="sm"
|
||||
:loading="loading"
|
||||
:confirm-loading="submitting"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="12">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem v-if="isEditMode" label="产品编码" prop="code">
|
||||
<ElInput
|
||||
:model-value="model.code"
|
||||
@@ -186,12 +187,12 @@ watch(visible, async value => {
|
||||
<ElInput v-model="model.code" clearable placeholder="不填则由后端自动生成" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="产品名称" prop="name">
|
||||
<ElInput v-model="model.name" clearable maxlength="128" placeholder="请输入产品名称" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="产品方向" prop="directionCode">
|
||||
<DictSelect
|
||||
v-model="model.directionCode"
|
||||
@@ -201,7 +202,7 @@ watch(visible, async value => {
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem v-if="isEditMode">
|
||||
<template #label>
|
||||
<span class="business-form-label-with-tip">
|
||||
@@ -225,9 +226,11 @@ watch(visible, async value => {
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-else label="产品经理" prop="managerUserId">
|
||||
<ElSelect v-model="model.managerUserId" clearable filterable placeholder="请选择产品经理">
|
||||
<ElOption v-for="item in managerUserOptions" :key="item.id" :label="item.nickname" :value="item.id" />
|
||||
</ElSelect>
|
||||
<BusinessUserSelect
|
||||
v-model="model.managerUserId"
|
||||
:options="managerUserOptions"
|
||||
placeholder="请选择产品经理"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { RDMS_OBJECT_DIRECTION_DICT_CODE } from '@/constants/dict';
|
||||
import DictSelect from '@/components/custom/dict-select.vue';
|
||||
import TableSearchPanel from '@/components/custom/table-search-panel.vue';
|
||||
import TableSearchFields, { type SearchField } from '@/components/custom/table-search-fields.vue';
|
||||
|
||||
defineOptions({ name: 'ProductSearch' });
|
||||
|
||||
@@ -9,7 +9,7 @@ interface Props {
|
||||
managerOptions: Api.SystemManage.UserSimple[];
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'reset'): void;
|
||||
@@ -20,6 +20,32 @@ const emit = defineEmits<Emits>();
|
||||
|
||||
const model = defineModel<Api.Product.ProductSearchParams>('model', { required: true });
|
||||
|
||||
const fields = computed<SearchField[]>(() => [
|
||||
{
|
||||
key: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '产品名称 / 编号'
|
||||
},
|
||||
{
|
||||
key: 'managerUserId',
|
||||
label: '产品经理',
|
||||
type: 'select',
|
||||
options: props.managerOptions.map(item => ({
|
||||
label: item.nickname,
|
||||
value: item.id
|
||||
})),
|
||||
placeholder: '筛选产品经理'
|
||||
},
|
||||
{
|
||||
key: 'directionCode',
|
||||
label: '产品方向',
|
||||
type: 'dict',
|
||||
dictCode: RDMS_OBJECT_DIRECTION_DICT_CODE,
|
||||
placeholder: '筛选产品方向'
|
||||
}
|
||||
]);
|
||||
|
||||
function reset() {
|
||||
emit('reset');
|
||||
}
|
||||
@@ -30,30 +56,7 @@ function search() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableSearchPanel :model="model" :action-col-lg="6" @reset="reset" @search="search">
|
||||
<ElCol :lg="6" :md="12" :sm="12">
|
||||
<ElFormItem label="关键词">
|
||||
<ElInput v-model="model.keyword" clearable placeholder="产品名称 / 编号" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :lg="6" :md="12" :sm="12">
|
||||
<ElFormItem label="产品经理">
|
||||
<ElSelect v-model="model.managerUserId" clearable filterable placeholder="筛选产品经理">
|
||||
<ElOption v-for="item in managerOptions" :key="item.id" :label="item.nickname" :value="item.id" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :lg="6" :md="12" :sm="12">
|
||||
<ElFormItem label="产品方向">
|
||||
<DictSelect
|
||||
v-model="model.directionCode"
|
||||
:dict-code="RDMS_OBJECT_DIRECTION_DICT_CODE"
|
||||
filterable
|
||||
placeholder="筛选产品方向"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</TableSearchPanel>
|
||||
<TableSearchFields v-model="model" :fields="fields" :columns="3" @reset="reset" @search="search" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -3,10 +3,7 @@ import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import type { TableInstance } from 'element-plus';
|
||||
import { ElButton, ElTag } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
RDMS_REQ_CATEGORY_DICT_CODE,
|
||||
RDMS_REQ_PRIORITY_DICT_CODE
|
||||
} from '@/constants/dict';
|
||||
import { RDMS_REQ_CATEGORY_DICT_CODE, RDMS_REQ_PRIORITY_DICT_CODE } from '@/constants/dict';
|
||||
import {
|
||||
fetchChangeRequirementStatus,
|
||||
fetchDeleteRequirement,
|
||||
@@ -90,7 +87,7 @@ function formatDateTime(value?: string | null) {
|
||||
}
|
||||
|
||||
function isTerminalStatus(statusCode: string) {
|
||||
return terminalStatusOptions.value.some(option => option === statusCode);
|
||||
return terminalStatusOptions.value.includes(statusCode);
|
||||
}
|
||||
|
||||
function canSplitRequirement(row: Api.Product.Requirement) {
|
||||
@@ -287,9 +284,7 @@ const columns = computed(() => [
|
||||
width: 100,
|
||||
align: 'center',
|
||||
formatter: (row: Api.Product.Requirement) => (
|
||||
<ElTag type={getRequirementStatusTagType(row.statusCode)}>
|
||||
{getStatusLabel(row.statusCode)}
|
||||
</ElTag>
|
||||
<ElTag type={getRequirementStatusTagType(row.statusCode)}>{getStatusLabel(row.statusCode)}</ElTag>
|
||||
)
|
||||
},
|
||||
{
|
||||
|
||||
@@ -423,7 +423,9 @@ watch(
|
||||
:member="selectedMember"
|
||||
:current-manager="currentManager"
|
||||
:role-options="roleOptions"
|
||||
:user-options="userOptions"
|
||||
:user-options="
|
||||
userOptions.filter(user => !members.some(member => member.status === 0 && member.userId === user.id))
|
||||
"
|
||||
@submit="handleSubmitMemberOperate"
|
||||
/>
|
||||
<MemberRemoveDialog
|
||||
|
||||
@@ -95,7 +95,7 @@ watch(
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
title="编辑基础信息"
|
||||
preset="lg"
|
||||
preset="sm"
|
||||
:confirm-disabled="confirmDisabled"
|
||||
@confirm="handleConfirm"
|
||||
>
|
||||
@@ -103,12 +103,42 @@ watch(
|
||||
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="12">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="产品编码">
|
||||
<ElInput :model-value="baseInfo?.code || ''" readonly class="base-info-dialog__readonly-input" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="产品名称" prop="name">
|
||||
<ElInput v-if="baseInfoEditable" v-model="model.name" maxlength="128" placeholder="请输入产品名称" />
|
||||
<ElInput
|
||||
v-else
|
||||
:model-value="model.name"
|
||||
readonly
|
||||
class="base-info-dialog__readonly-input"
|
||||
placeholder="未获取到产品名称"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="产品方向" prop="directionCode">
|
||||
<DictSelect
|
||||
v-if="baseInfoEditable"
|
||||
v-model="model.directionCode"
|
||||
:dict-code="RDMS_OBJECT_DIRECTION_DICT_CODE"
|
||||
filterable
|
||||
placeholder="请选择产品方向"
|
||||
/>
|
||||
<ElInput
|
||||
v-else
|
||||
:model-value="directionDisplayName"
|
||||
readonly
|
||||
class="base-info-dialog__readonly-input"
|
||||
placeholder="未获取到产品方向"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<span class="business-form-label-with-tip">
|
||||
@@ -131,36 +161,6 @@ watch(
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="产品名称" prop="name">
|
||||
<ElInput v-if="baseInfoEditable" v-model="model.name" maxlength="128" placeholder="请输入产品名称" />
|
||||
<ElInput
|
||||
v-else
|
||||
:model-value="model.name"
|
||||
readonly
|
||||
class="base-info-dialog__readonly-input"
|
||||
placeholder="未获取到产品名称"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="产品方向" prop="directionCode">
|
||||
<DictSelect
|
||||
v-if="baseInfoEditable"
|
||||
v-model="model.directionCode"
|
||||
:dict-code="RDMS_OBJECT_DIRECTION_DICT_CODE"
|
||||
filterable
|
||||
placeholder="请选择产品方向"
|
||||
/>
|
||||
<ElInput
|
||||
v-else
|
||||
:model-value="directionDisplayName"
|
||||
readonly
|
||||
class="base-info-dialog__readonly-input"
|
||||
placeholder="未获取到产品方向"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="产品描述">
|
||||
<ElInput
|
||||
|
||||
@@ -3,6 +3,7 @@ import { computed, nextTick, reactive, watch } from 'vue';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import BusinessFormSection from '@/components/custom/business-form-section.vue';
|
||||
import BusinessUserSelect from '@/components/custom/business-user-select.vue';
|
||||
import { getPreviousManagerRoleOptions, shouldRequireManagerHandover } from '../shared';
|
||||
|
||||
defineOptions({ name: 'MemberOperateDialog' });
|
||||
@@ -136,21 +137,24 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog v-model="visible" :title="dialogTitle" preset="lg" @confirm="handleConfirm">
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
|
||||
<BusinessFormDialog v-model="visible" :title="dialogTitle" preset="sm" @confirm="handleConfirm">
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top" :validate-on-rule-change="false">
|
||||
<BusinessFormSection title="成员信息">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="12">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem v-if="mode === 'create'" label="成员用户" prop="userId">
|
||||
<ElSelect v-model="model.userId" class="w-full" filterable placeholder="请选择成员用户">
|
||||
<ElOption v-for="item in userOptions" :key="item.id" :label="item.nickname" :value="item.id" />
|
||||
</ElSelect>
|
||||
<BusinessUserSelect v-model="model.userId" :options="userOptions" placeholder="请选择成员用户" />
|
||||
</ElFormItem>
|
||||
<ElFormItem v-else label="成员用户">
|
||||
<ElInput :model-value="member?.userNickname || userLabelMap.get(member?.userId || '') || ''" readonly />
|
||||
<ElInput
|
||||
:model-value="member?.userNickname || userLabelMap.get(member?.userId || '') || ''"
|
||||
readonly
|
||||
class="member-operate-dialog__readonly-input"
|
||||
placeholder="未获取到成员用户"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="目标角色" prop="roleId">
|
||||
<ElSelect v-model="model.roleId" class="w-full" placeholder="请选择角色">
|
||||
<ElOption v-for="item in roleOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||||
@@ -201,3 +205,22 @@ watch(
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.member-operate-dialog__readonly-input .el-input__wrapper) {
|
||||
background: linear-gradient(180deg, rgb(241 245 249 / 98%), rgb(226 232 240 / 94%)), rgb(241 245 249);
|
||||
box-shadow: 0 0 0 1px rgb(203 213 225 / 96%) inset;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
:deep(.member-operate-dialog__readonly-input .el-input__wrapper:hover),
|
||||
:deep(.member-operate-dialog__readonly-input.is-focus .el-input__wrapper) {
|
||||
box-shadow: 0 0 0 1px rgb(203 213 225 / 96%) inset;
|
||||
}
|
||||
|
||||
:deep(.member-operate-dialog__readonly-input .el-input__inner) {
|
||||
color: rgb(51 65 85 / 96%);
|
||||
cursor: default;
|
||||
-webkit-text-fill-color: rgb(51 65 85 / 96%);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -134,7 +134,7 @@ function getMemberStatusTagType(status: Api.Product.ProductMemberStatus) {
|
||||
:disabled="row.status !== 0 || row.managerFlag"
|
||||
@click="emit('edit', row)"
|
||||
>
|
||||
调整角色
|
||||
编辑
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-auth="{ code: 'project:product:update', source: 'object' }"
|
||||
|
||||
Reference in New Issue
Block a user