1105 lines
26 KiB
Vue
1105 lines
26 KiB
Vue
<template>
|
||
<view class="project-form-page">
|
||
<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>
|
||
|
||
<view class="loading-box" v-if="pageLoading">
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
|
||
<view v-else>
|
||
<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">
|
||
<text class="form-label ">项目金额(元)</text>
|
||
<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">
|
||
<text class="form-label ">客户</text>
|
||
<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"
|
||
:class="{ active: member.role === 'FOLLOWER' }"
|
||
@click="updateMemberRole(member.userId, 'FOLLOWER')"
|
||
>
|
||
<text class="radio"></text>
|
||
<text>跟进人</text>
|
||
</label>
|
||
<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>
|
||
</view>
|
||
<view class="empty-tip" v-if="!formData.memberList.length">
|
||
请点击「选择成员」添加项目成员
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<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 (
|
||
!!formData.value.name
|
||
// &&
|
||
// !!formData.value.customerId &&
|
||
// !!formData.value.amount &&
|
||
// formData.value.memberList.length > 0 &&
|
||
// !submitting.value
|
||
);
|
||
});
|
||
|
||
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 || '',
|
||
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 || '',
|
||
expireTime: merged.expireTime,
|
||
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,
|
||
expireTime: formData.value.expireTime,
|
||
projectTags: formData.value.projectTags || '',
|
||
remark: formData.value.remark || '',
|
||
developmentCost: parseNumber(formData.value.developmentCost),
|
||
developmentTime: formatDateTimeForPayload(formData.value.developmentTime),
|
||
middleCost: parseNumber(formData.value.middleCost),
|
||
middleTime: formatDateTimeForPayload(formData.value.middleTime),
|
||
afterCost: parseNumber(formData.value.afterCost),
|
||
afterTime: formatDateTimeForPayload(formData.value.afterTime),
|
||
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 '';
|
||
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`;
|
||
};
|
||
|
||
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 {
|
||
|
||
padding: 20px 20px;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
background: #fff;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.navbar-content {
|
||
margin-top: 15px;
|
||
width: 100%;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
|
||
}
|
||
|
||
.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;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.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 {
|
||
width: 140px;
|
||
}
|
||
|
||
.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;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.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;
|
||
max-height: 90%;
|
||
}
|
||
|
||
.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 {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.search-input {
|
||
width: 100%;
|
||
background: #f5f6f7;
|
||
border-radius: 10px;
|
||
padding: 10px 14px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.member-list {
|
||
height: 600px;
|
||
margin: 12px 0;
|
||
}
|
||
|
||
.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;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-top: 12px;
|
||
}
|
||
</style>
|
||
|