OfficeSystem/pages/project/form/index.vue

1105 lines
26 KiB
Vue
Raw Normal View History

2025-11-17 16:58:11 +08:00
<template>
<view class="project-form-page">
2025-11-18 17:19:03 +08:00
<!-- <view class="custom-navbar">-->
<!-- <view class="navbar-content">-->
<!-- <text class="nav-btn" @click="handleBack">取消</text>-->
<!-- <text class="nav-title">{{ pageTitle }}</text>-->
<!-- <text-->
<!-- class="nav-btn"-->
<!-- :class="{ disabled: !canSubmit || submitting }"-->
<!-- @click="handleSubmit"-->
<!-- >-->
<!-- {{ mode === 'edit' ? '保存' : '创建' }}-->
<!-- </text>-->
<!-- </view>-->
<!-- </view>-->
2025-11-17 16:58:11 +08:00
<view class="loading-box" v-if="pageLoading">
<text class="loading-text">加载中...</text>
</view>
<view v-else>
2025-11-17 16:58:11 +08:00
<view class="form-container">
<view class="form-section">
<view class="section-title">基本信息</view>
<view class="form-item">
<text class="form-label required">项目名称</text>
<input
class="form-input"
v-model.trim="formData.name"
placeholder="请输入项目名称"
placeholder-style="color:#999;"
/>
</view>
<view class="form-item">
<text class="form-label">项目编号</text>
<input
class="form-input"
v-model.trim="formData.no"
placeholder="可填写项目编号"
placeholder-style="color:#999;"
/>
</view>
<view class="form-row">
<view class="form-item half">
2025-11-18 16:02:59 +08:00
<text class="form-label ">项目金额</text>
2025-11-17 16:58:11 +08:00
<input
class="form-input"
v-model="formData.amount"
type="digit"
placeholder="请输入金额"
placeholder-style="color:#999;"
/>
</view>
<view class="form-item half">
<text class="form-label">到账时间</text>
<view class="picker-input" @click="openDatePicker('expireTime')">
<text>{{ formData.expireTime || '请选择日期' }}</text>
<text class="picker-arrow"></text>
</view>
</view>
</view>
<view class="form-item">
2025-11-18 16:02:59 +08:00
<text class="form-label ">客户</text>
2025-11-17 16:58:11 +08:00
<view class="picker-input" @click="openCustomerPicker">
<text>{{ formData.customerName || '请选择客户' }}</text>
<text class="picker-arrow"></text>
</view>
</view>
<view class="form-item">
<text class="form-label">标签</text>
<input
class="form-input"
v-model.trim="formData.projectTags"
placeholder="请输入标签,多个以逗号分隔"
placeholder-style="color:#999;"
/>
</view>
<view class="form-item">
<text class="form-label">备注</text>
<textarea
class="form-textarea"
v-model="formData.remark"
placeholder="请输入备注"
placeholder-style="color:#999;"
/>
</view>
</view>
<view class="form-section">
<view class="section-title">费用规划</view>
<view
class="cost-block"
v-for="item in costFields"
:key="item.amountKey"
>
<view class="cost-row">
<text class="cost-title">{{ item.label }}</text>
<view class="cost-inputs">
<view class="cost-item">
<text class="cost-label">金额</text>
<input
class="form-input"
type="digit"
v-model="formData[item.amountKey]"
placeholder="请输入金额"
placeholder-style="color:#999;"
/>
</view>
<view class="cost-item">
<text class="cost-label">收款时间</text>
<view
class="picker-input"
@click="openDatePicker(item.dateKey)"
>
<text>{{ formData[item.dateKey] || '请选择日期' }}</text>
<text class="picker-arrow"></text>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="form-section">
<view class="section-title">附件</view>
<AttachmentFileUploader
v-model="formData.attaches"
:max-count="10"
title="上传附件"
/>
<text class="section-tip">
支持 png/jpg/jpeg/doc/docx/xls/xlsx/ppt/pdf/zip/rar 等格式单个不超过 200MB
</text>
</view>
<view class="form-section">
<view class="section-title member-header">
<text>项目成员</text>
<uv-button size="mini" type="primary" @click="openMemberModal">
选择成员
</uv-button>
</view>
<view class="member-table">
<view class="member-row header">
<text class="col index">序号</text>
<text class="col name">成员</text>
<text class="col role">角色</text>
<text class="col action">操作</text>
</view>
<scroll-view class="member-scroll" scroll-y>
<view
class="member-row"
v-for="(member, index) in formData.memberList"
:key="member.userId || index"
>
<text class="col index">{{ index + 1 }}</text>
<view class="col name member-name">
<text>{{ member.userName || '未命名' }}</text>
</view>
<view class="col role role-options">
<label
class="role-option"
:class="{ active: member.role === 'OWNER' }"
@click="updateMemberRole(member.userId, 'OWNER')"
>
<text class="radio"></text>
<text>负责人</text>
</label>
<label
class="role-option"
2025-11-18 16:12:32 +08:00
:class="{ active: member.role === 'FOLLOWER' }"
@click="updateMemberRole(member.userId, 'FOLLOWER')"
>
<text class="radio"></text>
<text>跟进人</text>
</label>
2025-11-18 16:12:32 +08:00
<label
class="role-option"
:class="{ active: member.role === 'NORMAL' }"
@click="updateMemberRole(member.userId, 'NORMAL')"
>
<text class="radio"></text>
<text>普通成员</text>
</label>
</view>
<text class="col action action-btn" @click="removeMember(member.userId)">
删除
</text>
2025-11-17 16:58:11 +08:00
</view>
<view class="empty-tip" v-if="!formData.memberList.length">
请点击选择成员添加项目成员
2025-11-17 16:58:11 +08:00
</view>
</scroll-view>
2025-11-17 16:58:11 +08:00
</view>
</view>
</view>
</view>
2025-11-17 16:58:11 +08:00
<view class="bottom-bar">
<uv-button
type="primary"
:disabled="!canSubmit || submitting"
:loading="submitting"
@click="handleSubmit"
>
{{ mode === 'edit' ? '保存修改' : '创建项目' }}
</uv-button>
</view>
<uv-picker
ref="customerPickerRef"
:columns="customerColumns"
keyName="label"
@confirm="handleCustomerConfirm"
></uv-picker>
<uv-datetime-picker
ref="datePickerRef"
v-model="datePickerValue"
:mode="datePickerMode"
@confirm="onDateConfirm"
></uv-datetime-picker>
<view class="member-modal" v-if="showMemberModal">
<view class="modal-mask" @click="closeMemberModal"></view>
<view class="modal-panel">
<view class="modal-header">
<text class="modal-title">选择成员</text>
<text class="modal-close" @click="closeMemberModal"></text>
</view>
<view class="search-box">
<input
v-model.trim="memberKeyword"
class="search-input"
placeholder="搜索姓名或部门"
placeholder-style="color:#999;"
/>
</view>
<scroll-view class="member-list" scroll-y>
<view
class="member-item"
v-for="user in filteredMemberOptions"
:key="user.userId"
@click="toggleMember(user.userId)"
>
<view class="member-info">
<text class="member-name-text">{{ user.userName }}</text>
<text class="member-dept" v-if="user.deptName">{{ user.deptName }}</text>
</view>
<view
class="select-indicator"
:class="{ active: selectedMemberIds.includes(user.userId) }"
></view>
</view>
<view class="empty-tip" v-if="!filteredMemberOptions.length">
<text>暂无匹配的成员</text>
</view>
</scroll-view>
<view class="modal-actions">
<uv-button @click="closeMemberModal">取消</uv-button>
<uv-button type="primary" @click="confirmMemberSelection">确定</uv-button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import AttachmentFileUploader from '@/components/task/AttachmentFileUploader.vue';
import {
getCustomerList,
getUserList,
getProjectDetail,
createProject,
updateProject
} from '@/api';
const costFields = [
{ label: '前期费用', amountKey: 'developmentCost', dateKey: 'developmentTime' },
{ label: '中期费用', amountKey: 'middleCost', dateKey: 'middleTime' },
{ label: '后期费用', amountKey: 'afterCost', dateKey: 'afterTime' }
];
const mode = ref('add');
const pageTitle = computed(() => (mode.value === 'edit' ? '编辑项目' : '新建项目'));
const pageLoading = ref(true);
const submitting = ref(false);
const projectId = ref('');
const formData = ref(createDefaultForm());
const customerOptions = ref([]);
const customerPickerRef = ref(null);
const customerColumns = computed(() => [customerOptions.value]);
const memberOptions = ref([]);
const memberKeyword = ref('');
const showMemberModal = ref(false);
const selectedMemberIds = ref([]);
const datePickerRef = ref(null);
const datePickerValue = ref(Date.now());
const datePickerMode = ref('date');
const pendingDateField = ref('');
const canSubmit = computed(() => {
return (
2025-11-18 16:02:59 +08:00
!!formData.value.name
// &&
// !!formData.value.customerId &&
// !!formData.value.amount &&
// formData.value.memberList.length > 0 &&
// !submitting.value
2025-11-17 16:58:11 +08:00
);
});
const filteredMemberOptions = computed(() => {
if (!memberKeyword.value) return memberOptions.value;
const keyword = memberKeyword.value.toLowerCase();
return memberOptions.value.filter((item) => {
const name = (item.userName || '').toLowerCase();
const dept = (item.deptName || '').toLowerCase();
return name.includes(keyword) || dept.includes(keyword);
});
});
onLoad(async (options) => {
if (options?.mode) {
mode.value = options.mode === 'edit' ? 'edit' : 'add';
}
if (options?.id) {
projectId.value = options.id;
mode.value = 'edit';
}
try {
await Promise.all([loadCustomerOptions(), loadMemberOptions()]);
if (mode.value === 'edit' && projectId.value) {
await loadProjectDetail(projectId.value);
}
} finally {
pageLoading.value = false;
}
});
const handleBack = () => {
uni.navigateBack();
};
const openCustomerPicker = async () => {
if (!customerOptions.value.length) {
await loadCustomerOptions();
}
if (!customerOptions.value.length) {
uni.showToast({
title: '暂无可选客户',
icon: 'none'
});
return;
}
if (customerPickerRef.value?.open) {
customerPickerRef.value.open();
}
};
const handleCustomerConfirm = ({ value }) => {
const selected = value?.[0];
if (selected) {
formData.value.customerId = String(selected.value);
formData.value.customerName = selected.label;
}
};
const openDatePicker = (field) => {
pendingDateField.value = field;
datePickerMode.value = 'date';
datePickerValue.value = formData.value[field]
? new Date(formData.value[field].replace(/-/g, '/')).getTime()
: Date.now();
if (datePickerRef.value?.open) {
datePickerRef.value.open();
}
};
const onDateConfirm = (event) => {
if (!pendingDateField.value || !event?.value) return;
formData.value[pendingDateField.value] = formatDate(event.value);
};
const openMemberModal = () => {
selectedMemberIds.value = formData.value.memberList
.map((member) => member.userId)
.filter((id) => id);
memberKeyword.value = '';
showMemberModal.value = true;
};
const closeMemberModal = () => {
showMemberModal.value = false;
};
const toggleMember = (userId) => {
const id = String(userId);
const index = selectedMemberIds.value.indexOf(id);
if (index >= 0) {
selectedMemberIds.value.splice(index, 1);
} else {
selectedMemberIds.value.push(id);
}
};
const confirmMemberSelection = () => {
const existingMap = new Map(
formData.value.memberList.map((member) => [String(member.userId), member])
);
const selected = memberOptions.value.filter((user) =>
selectedMemberIds.value.includes(user.userId)
);
formData.value.memberList = selected.map((user) => {
const persisted = existingMap.get(user.userId);
return {
userId: user.userId,
userName: user.userName,
userAvatar: user.avatar || user.userAvatar || '',
role: persisted?.role || 'NORMAL'
};
});
showMemberModal.value = false;
};
const removeMember = (userId) => {
formData.value.memberList = formData.value.memberList.filter(
(member) => member.userId !== userId
);
};
const updateMemberRole = (userId, role) => {
formData.value.memberList = formData.value.memberList.map((member) => {
if (member.userId === userId) {
return { ...member, role };
}
return member;
});
};
const handleSubmit = async () => {
if (!canSubmit.value || submitting.value) {
if (!canSubmit.value) {
uni.showToast({
title: '请完善必填信息',
icon: 'none'
});
}
return;
}
submitting.value = true;
uni.showLoading({
title: mode.value === 'edit' ? '保存中...' : '创建中...'
});
const payload = buildPayload();
try {
if (mode.value === 'edit') {
payload.id = projectId.value || payload.id;
await updateProject(payload);
} else {
await createProject(payload);
}
uni.showToast({
title: mode.value === 'edit' ? '保存成功' : '创建成功',
icon: 'success'
});
uni.$emit('projectListRefresh');
setTimeout(() => {
uni.navigateBack();
}, 600);
} catch (error) {
console.error('保存项目失败:', error);
} finally {
submitting.value = false;
uni.hideLoading();
}
};
const loadCustomerOptions = async () => {
try {
const res = await getCustomerList({ pageNum: 1, pageSize: 200 });
customerOptions.value = (res.rows || []).map((item) => ({
label: item.name,
value: String(item.id),
raw: item
}));
} catch (error) {
console.error('加载客户列表失败:', error);
uni.showToast({
title: '加载客户失败',
icon: 'none'
});
}
};
const loadMemberOptions = async () => {
try {
const res = await getUserList({ pageSize: 200 });
memberOptions.value = (res.rows || res.data || res.list || []).map((item) => ({
userId: String(item.userId || item.id),
userName: item.nickName || '',
2025-11-17 16:58:11 +08:00
deptName: item.deptName || item.dept?.deptName || '',
avatar: item.avatar || item.userAvatar || ''
}));
} catch (error) {
console.error('加载成员列表失败:', error);
uni.showToast({
title: '加载成员失败',
icon: 'none'
});
}
};
const loadProjectDetail = async (id) => {
try {
const detail = await getProjectDetail(id);
if (!detail) return;
const merged = {
...createDefaultForm(),
...detail
};
formData.value = {
...formData.value,
id: merged.id || id,
no: merged.no || '',
name: merged.name || merged.projectName || '',
amount: merged.amount != null ? String(merged.amount) : '',
customerId: merged.customerId ? String(merged.customerId) : merged.customerId,
customerName: merged.customerName || '',
2025-11-18 16:02:59 +08:00
expireTime: merged.expireTime,
2025-11-17 16:58:11 +08:00
projectTags: merged.projectTags || '',
remark: merged.remark || '',
developmentCost: safeNumberToString(merged.developmentCost),
middleCost: safeNumberToString(merged.middleCost),
afterCost: safeNumberToString(merged.afterCost),
developmentTime: normalizeDateString(merged.developmentTime),
middleTime: normalizeDateString(merged.middleTime),
afterTime: normalizeDateString(merged.afterTime),
attaches: normalizeAttaches(merged.attaches),
memberList: normalizeMembers(merged.memberList)
};
} catch (error) {
console.error('获取项目详情失败:', error);
uni.showToast({
title: '加载项目失败',
icon: 'none'
});
}
};
function createDefaultForm() {
return {
id: null,
no: '',
name: '',
amount: '',
customerId: '',
customerName: '',
expireTime: '',
projectTags: '',
remark: '',
developmentCost: '',
middleCost: '',
afterCost: '',
developmentTime: '',
middleTime: '',
afterTime: '',
attaches: [],
memberList: []
};
}
const buildPayload = () => {
const payload = {
id: formData.value.id,
no: formData.value.no || null,
name: formData.value.name,
amount: parseNumber(formData.value.amount),
customerId: formData.value.customerId,
customerName: formData.value.customerName,
2025-11-18 16:02:59 +08:00
expireTime: formData.value.expireTime,
2025-11-17 16:58:11 +08:00
projectTags: formData.value.projectTags || '',
remark: formData.value.remark || '',
developmentCost: parseNumber(formData.value.developmentCost),
2025-11-18 16:02:59 +08:00
developmentTime: formatDateTimeForPayload(formData.value.developmentTime),
2025-11-17 16:58:11 +08:00
middleCost: parseNumber(formData.value.middleCost),
2025-11-18 16:02:59 +08:00
middleTime: formatDateTimeForPayload(formData.value.middleTime),
2025-11-17 16:58:11 +08:00
afterCost: parseNumber(formData.value.afterCost),
2025-11-18 16:02:59 +08:00
afterTime: formatDateTimeForPayload(formData.value.afterTime),
2025-11-17 16:58:11 +08:00
attaches: formData.value.attaches.map((file, index) => ({
name: file.name || getFileNameFromUrl(file.path),
url: file.path,
uid: file.uid || `${Date.now()}_${index}`,
status: 'success'
})),
memberList: formData.value.memberList.map((member) => ({
userId: member.userId,
userName: member.userName,
userAvatar: member.userAvatar || '',
role: member.role || 'NORMAL'
}))
};
return payload;
};
const parseNumber = (value) => {
if (value === null || value === undefined || value === '') return null;
const num = Number(value);
return Number.isNaN(num) ? null : num;
};
const safeNumberToString = (value) => {
if (value === null || value === undefined || value === '') return '';
return String(value);
};
const normalizeDateString = (value) => {
if (!value) return '';
2025-11-18 16:02:59 +08:00
return value.replace('T', ' ').split(' ')[0];
};
const formatDateTimeForPayload = (value) => {
if (!value) return null;
const cleaned = value.replace('T', ' ').trim();
if (cleaned.includes(':')) {
return cleaned;
}
return `${cleaned} 00:00:00`;
2025-11-17 16:58:11 +08:00
};
const normalizeAttaches = (value) => {
if (!value) return [];
if (Array.isArray(value)) {
return value
.map((item, index) => ({
name: item.name || getFileNameFromUrl(item.url || item.path),
path: item.url || item.path,
size: item.size || 0,
uid: item.uid || `${Date.now()}_${index}`
}))
.filter((item) => item.path);
}
if (typeof value === 'string') {
return value
.split(',')
.map((url, index) => url.trim())
.filter((url) => url)
.map((url, index) => ({
name: getFileNameFromUrl(url),
path: url,
size: 0,
uid: `${Date.now()}_${index}`
}));
}
return [];
};
const normalizeMembers = (value) => {
if (!Array.isArray(value)) return [];
return value
.map((member) => ({
userId: String(member.userId || member.id || ''),
userName: member.userName || member.name || '',
userAvatar: member.userAvatar || member.avatar || '',
role: member.role || 'NORMAL'
}))
.filter((member) => member.userId);
};
const getFileNameFromUrl = (url = '') => {
if (!url) return '附件';
const parts = url.split('/');
return parts[parts.length - 1] || '附件';
};
const formatDate = (timestamp) => {
const date = new Date(timestamp);
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
};
</script>
<style scoped lang="scss">
.project-form-page {
min-height: 100vh;
background: #f5f5f5;
display: flex;
flex-direction: column;
}
.custom-navbar {
2025-11-18 16:02:59 +08:00
padding: 20px 20px;
2025-11-17 16:58:11 +08:00
display: flex;
align-items: flex-end;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.navbar-content {
2025-11-18 16:02:59 +08:00
margin-top: 15px;
2025-11-17 16:58:11 +08:00
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
2025-11-18 16:02:59 +08:00
2025-11-17 16:58:11 +08:00
}
.nav-btn {
font-size: 15px;
color: #1677ff;
min-width: 48px;
text-align: center;
&.disabled {
color: #ccc;
}
}
.nav-title {
font-size: 16px;
font-weight: 600;
color: #111;
}
.loading-box {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.loading-text {
color: #666;
}
.form-scroll {
flex: 1;
}
.form-container {
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.form-section {
background: #fff;
border-radius: 12px;
padding: 16px;
}
.section-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 12px;
color: #111;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.form-item {
display: flex;
flex-direction: column;
margin-bottom: 12px;
gap: 8px;
&:last-child {
margin-bottom: 0;
}
}
.form-row {
display: flex;
gap: 12px;
}
.form-item.half {
flex: 1;
}
.form-label {
font-size: 13px;
color: #666;
}
.form-label.required::after {
content: '*';
color: #ff4d4f;
margin-left: 4px;
}
.form-input,
.form-textarea,
.picker-input {
width: 100%;
background: #f7f8fa;
border-radius: 8px;
padding: 0 12px;
min-height: 40px;
display: flex;
align-items: center;
font-size: 14px;
color: #111;
2025-11-18 16:02:59 +08:00
box-sizing: border-box;
2025-11-17 16:58:11 +08:00
}
.form-textarea {
min-height: 90px;
padding-top: 10px;
padding-bottom: 10px;
}
.picker-input {
justify-content: space-between;
}
.picker-arrow {
color: #999;
font-size: 18px;
}
.cost-block {
border-radius: 12px;
background: #f9fafc;
padding: 12px;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
}
.cost-row {
display: flex;
flex-direction: column;
gap: 12px;
}
.cost-title {
font-size: 14px;
font-weight: 600;
color: #333;
}
.cost-inputs {
display: flex;
gap: 12px;
}
.cost-item {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.cost-label {
font-size: 12px;
color: #666;
}
.section-tip {
display: block;
margin-top: 8px;
font-size: 12px;
color: #999;
}
.member-table {
border-radius: 12px;
border: 1px solid #f0f0f0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.member-scroll {
max-height: 400rpx;
min-height: 80rpx;
overflow-y: auto;
2025-11-17 16:58:11 +08:00
}
.member-row {
display: flex;
align-items: center;
padding: 12px;
background: #fff;
&.header {
background: #f5f6f7;
font-weight: 600;
color: #555;
}
&:not(.header) + .member-row {
border-top: 1px solid #f0f0f0;
}
}
.member-name {
font-weight: 500;
color: #333;
}
.col {
font-size: 13px;
color: #333;
}
.col.index {
width: 50px;
}
.col.name {
flex: 1;
}
.col.role {
2025-11-18 16:02:59 +08:00
width: 140px;
2025-11-17 16:58:11 +08:00
}
.col.action {
width: 60px;
text-align: right;
}
.action-btn {
color: #ff4d4f;
}
.empty-tip {
padding: 16px;
text-align: center;
color: #999;
font-size: 13px;
}
.role-options {
display: flex;
2025-11-18 16:12:32 +08:00
gap: 12px;
flex-wrap: wrap;
2025-11-17 16:58:11 +08:00
}
.role-option {
display: flex;
align-items: center;
gap: 6px;
color: #666;
font-size: 12px;
.radio {
width: 14px;
height: 14px;
border-radius: 50%;
border: 1px solid #bbb;
}
&.active {
color: #1677ff;
.radio {
border-color: #1677ff;
background: #1677ff;
}
}
}
.bottom-bar {
padding: 12px 16px;
background: #fff;
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.03);
}
.member-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
}
.modal-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
}
.modal-panel {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: #fff;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
padding: 16px;
display: flex;
flex-direction: column;
2025-11-18 16:12:32 +08:00
max-height: 90%;
2025-11-17 16:58:11 +08:00
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.modal-title {
font-size: 16px;
font-weight: 600;
}
.modal-close {
font-size: 20px;
color: #999;
}
.search-box {
2025-11-18 16:12:32 +08:00
margin-bottom: 12px;
2025-11-17 16:58:11 +08:00
}
.search-input {
width: 100%;
background: #f5f6f7;
border-radius: 10px;
padding: 10px 14px;
font-size: 14px;
}
.member-list {
2025-11-18 16:12:32 +08:00
height: 600px;
2025-11-18 16:02:59 +08:00
margin: 12px 0;
2025-11-17 16:58:11 +08:00
}
.member-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.member-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.member-name-text {
font-size: 14px;
color: #333;
}
.member-dept {
font-size: 12px;
color: #999;
}
.select-indicator {
width: 18px;
height: 18px;
border-radius: 50%;
border: 1px solid #ccc;
&.active {
border-color: #1677ff;
background: #1677ff;
}
}
.modal-actions {
display: flex;
2025-11-18 16:02:59 +08:00
justify-content: space-between;
2025-11-18 16:12:32 +08:00
gap: 12px;
margin-top: 12px;
2025-11-17 16:58:11 +08:00
}
</style>