feat(projects): 新增项目、执行、任务等功能

This commit is contained in:
2026-05-09 11:30:34 +08:00
parent f4f43814b3
commit 824392b564
106 changed files with 13060 additions and 1049 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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) {

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>
)
},
{

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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' }"