ykt-wxapp/pages/message/benefit-entry.vue

1111 lines
31 KiB
Vue

<template>
<full-page ref="pageRef" pageStyle="background:#f5f6f8" pageClass="benefit-page-shell">
<view class="benefit-page">
<view class="patient-card">
<view class="patient-card-accent"></view>
<view class="patient-card-main">
<view class="patient-card-top">
<view class="patient-name-wrap">
<text class="patient-name">{{ patientName || "当前患者" }}</text>
<text class="patient-badge">患者档案</text>
</view>
<text class="patient-status">会话已关联</text>
</view>
<view class="patient-meta">
<view class="patient-meta-item">
<text class="patient-meta-label">团队</text>
<text class="patient-meta-value">{{ teamName || "未关联团队" }}</text>
</view>
</view>
</view>
</view>
<view class="section-title">权益项目</view>
<view v-for="(entry, index) in entries" :id="`entry-${entry.key}`" :key="entry.key" class="entry-card">
<view class="field-label">项目名称</view>
<view class="project-input" @click="openProjectPicker(index)">
<text :class="entry.project ? 'value-text' : 'placeholder-text'">
{{ entry.project?.projectName || "请选择权益项目" }}
</text>
<uni-icons type="right" size="18" color="#999" />
</view>
<view v-if="showBenefitAmount" class="field-row">
<view class="field-block">
<view class="field-label">单价</view>
<input class="number-input" type="digit" v-model="entry.price" @input="calculateDiscount(entry)" />
</view>
<view class="field-block">
<view class="field-label">数量</view>
<input class="number-input" type="number" v-model="entry.usageCount" @input="calculateTotalPrice(entry)" />
</view>
</view>
<view v-if="showBenefitAmount" class="field-row">
<view class="field-block">
<view class="field-label">折扣</view>
<view class="discount-input-wrap">
<input class="number-input" type="digit" v-model="entry.discount" :disabled="entry.isFree" @input="calculateTotalPrice(entry)" />
<checkbox-group class="free-check" @change="handleFreeChange($event, entry)">
<label class="free-check-label">
<checkbox value="free" :checked="entry.isFree" color="#0877f1" />
<text>赠送</text>
</label>
</checkbox-group>
</view>
</view>
<view class="field-block">
<view class="field-label">总价</view>
<input class="number-input total-input" type="digit" v-model="entry.totalPrice" :disabled="entry.isFree" @input="calculateDiscount(entry)" />
</view>
</view>
<view v-else class="field-row">
<view class="field-block">
<view class="field-label">数量</view>
<input class="number-input" type="number" v-model="entry.usageCount" />
</view>
<view class="field-block">
<view class="field-label">有效期</view>
<view class="valid-time-wrap">
<view class="valid-time-picker" @tap="prepareValidTimePicker(entry)">
<picker
mode="multiSelector"
:range="validTimeRange"
:value="validTimeIndex"
@columnchange="onValidTimeColumnChange"
@change="changeValidTime($event, entry)"
>
<view class="picker-value" :class="entry.validTime ? 'value-text' : 'placeholder-text'">
{{ entry.validTime || "选择日期/长期" }}
</view>
</picker>
</view>
<view class="unlimited-button" @click="clearValidTime(entry)">长期</view>
</view>
</view>
</view>
<view v-if="showBenefitAmount" class="field-row">
<view class="field-block field-block-full">
<view class="field-label">有效期</view>
<view class="valid-time-wrap">
<view class="valid-time-picker" @tap="prepareValidTimePicker(entry)">
<picker
mode="multiSelector"
:range="validTimeRange"
:value="validTimeIndex"
@columnchange="onValidTimeColumnChange"
@change="changeValidTime($event, entry)"
>
<view class="picker-value" :class="entry.validTime ? 'value-text' : 'placeholder-text'">
{{ entry.validTime || "选择日期/长期" }}
</view>
</picker>
</view>
<view class="unlimited-button" @click="clearValidTime(entry)">长期</view>
</view>
</view>
</view>
<view class="field-group">
<view class="field-label">治疗科室</view>
<picker
:range="getProjectDepts(entry)"
range-key="deptName"
:disabled="!entry.project?._id || !getProjectDepts(entry).length"
@change="changeDept($event, entry)"
>
<view class="project-input">
<text :class="entry.dept ? 'value-text' : 'placeholder-text'">
{{ entry.dept?.deptName || (entry.project ? "该项目未配置治疗科室" : "请先选择权益项目") }}
</text>
<uni-icons type="right" size="18" color="#999" />
</view>
</picker>
</view>
<view class="field-group">
<view class="field-label">计划治疗医生</view>
<picker
:range="getTreatmentDoctors(entry)"
range-key="anotherName"
:disabled="!entry.dept?._id || !getTreatmentDoctors(entry).length"
@change="changeTreatmentDoctor($event, entry)"
>
<view class="project-input">
<text :class="entry.treatmentDoctorUserId ? 'value-text' : 'placeholder-text'">
{{ entry.treatmentDoctorName || (entry.dept ? "选择治疗医生" : "请先选择治疗科室") }}
</text>
<uni-icons type="right" size="18" color="#999" />
</view>
</picker>
</view>
<view v-if="entries.length > 1" class="remove-entry" @click="removeEntry(index)">删除此项目</view>
</view>
<view class="add-entry" @click="addEntry">
<uni-icons type="plusempty" size="18" color="#0877f1" />
<text>添加权益项目</text>
</view>
<view v-if="projectPickerVisible" class="picker-mask" @click="closeProjectPicker">
<view class="project-picker" @click.stop>
<view class="picker-header">
<text class="picker-title">选择权益项目</text>
<uni-icons type="closeempty" size="22" color="#666" @click="closeProjectPicker" />
</view>
<view class="search-box">
<uni-icons type="search" size="18" color="#999" />
<input v-model="projectKeyword" class="search-input" placeholder="输入项目名称搜索" @input="searchProjects" />
</view>
<scroll-view scroll-y class="project-list" @scrolltolower="loadMoreProjects">
<view v-for="project in projectList" :key="project._id" class="project-item"
@click="selectProject(project)">
<view class="project-info">
<view class="project-name">{{ project.projectName }}</view>
<view class="project-depts">{{ project.deptNames || "未配置治疗科室" }}</view>
</view>
<view v-if="showBenefitAmount" class="project-price">¥{{ Number(project.price || 0).toFixed(2) }}</view>
</view>
<view v-if="!projectLoading && !projectList.length" class="empty-text">暂无可用权益项目</view>
<view v-if="projectLoading" class="empty-text">加载中...</view>
<view v-else-if="projectList.length && !projectHasMore" class="empty-text">已加载全部权益项目</view>
</scroll-view>
</view>
</view>
</view>
<template #footer>
<view class="footer-bar">
<button class="cancel-button" @click="goBack">取消</button>
<button class="submit-button" :loading="saving" @click="submitEntries">生成权益</button>
</view>
</template>
</full-page>
</template>
<script setup>
import { nextTick, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import api from "@/utils/api.js";
import useAccountStore from "@/store/account.js";
import fullPage from "@/components/full-page.vue";
const PENDING_FOLLOWUP_SEND_STORAGE_KEY = "ykt_followup_pending_send";
const accountStore = useAccountStore();
const pageRef = ref(null);
const groupId = ref("");
const patientId = ref("");
const patientName = ref("");
const teamId = ref("");
const teamName = ref("");
const corpId = ref("");
const showBenefitAmount = ref(true);
const staffList = ref([]);
const entries = ref([]);
const saving = ref(false);
const projectPickerVisible = ref(false);
const projectPickerIndex = ref(-1);
const projectKeyword = ref("");
const projectList = ref([]);
const projectLoading = ref(false);
const projectHasMore = ref(true);
const projectPage = ref(1);
const projectPageSize = 30;
const VALID_YEAR_SPAN = 50;
const validTimeRange = ref([[], [], []]);
const validTimeIndex = ref([0, 0, 0]);
let searchTimer = null;
let entryKey = 0;
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function getTodayParts() {
const now = new Date();
return {
year: now.getFullYear(),
month: now.getMonth() + 1,
day: now.getDate(),
};
}
function getDaysInMonth(year, month) {
return new Date(year, month, 0).getDate();
}
function buildYearOptions() {
const { year } = getTodayParts();
return Array.from({ length: VALID_YEAR_SPAN + 1 }, (_, index) => `${year + index}`);
}
function buildMonthOptions(year) {
const today = getTodayParts();
const startMonth = year === today.year ? today.month : 1;
return Array.from({ length: 12 - startMonth + 1 }, (_, index) => `${startMonth + index}`);
}
function buildDayOptions(year, month) {
const today = getTodayParts();
const startDay = year === today.year && month === today.month ? today.day : 1;
const maxDay = getDaysInMonth(year, month);
return Array.from({ length: maxDay - startDay + 1 }, (_, index) => `${startDay + index}`);
}
function parseValidTime(value) {
const today = getTodayParts();
if (!value) return today;
const [year, month, day] = String(value).split("-").map(Number);
if (!year || !month || !day) return today;
const selected = { year, month, day };
const selectedText = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
return selectedText < formatDate(new Date()) ? today : selected;
}
function syncValidTimePicker(selected) {
const years = buildYearOptions();
const year = selected.year;
const months = buildMonthOptions(year);
const monthValues = months.map((item) => Number(item.replace("月", "")));
const month = monthValues.includes(selected.month) ? selected.month : monthValues[0];
const days = buildDayOptions(year, month);
const dayValues = days.map((item) => Number(item.replace("日", "")));
const day = dayValues.includes(selected.day) ? selected.day : dayValues[0];
validTimeRange.value = [years, months, days];
validTimeIndex.value = [
years.findIndex((item) => Number(item.replace("年", "")) === year),
monthValues.indexOf(month),
dayValues.indexOf(day),
];
}
function prepareValidTimePicker(entry) {
syncValidTimePicker(parseValidTime(entry.validTime));
}
function onValidTimeColumnChange(event) {
const { column, value } = event.detail;
const nextIndex = [...validTimeIndex.value];
nextIndex[column] = value;
const year = Number(validTimeRange.value[0][nextIndex[0]].replace("年", ""));
if (column === 0) {
const months = buildMonthOptions(year);
const month = Number(months[0].replace("月", ""));
const days = buildDayOptions(year, month);
validTimeRange.value = [validTimeRange.value[0], months, days];
nextIndex[1] = 0;
nextIndex[2] = 0;
} else if (column === 1) {
const month = Number(validTimeRange.value[1][nextIndex[1]].replace("月", ""));
const days = buildDayOptions(year, month);
validTimeRange.value = [validTimeRange.value[0], validTimeRange.value[1], days];
nextIndex[2] = Math.min(nextIndex[2], days.length - 1);
}
validTimeIndex.value = nextIndex;
}
prepareValidTimePicker({ validTime: "" });
function createEntry() {
return {
key: ++entryKey,
project: null,
price: 0,
totalPrice: 0,
discount: 10,
isFree: false,
usageCount: 1,
validTime: "",
dept: null,
treatmentDoctorUserId: "",
treatmentDoctorName: "",
};
}
function decodeRouteParam(value = "") {
try {
return decodeURIComponent(value || "");
} catch (error) {
return value || "";
}
}
onLoad((options = {}) => {
groupId.value = decodeRouteParam(options.groupId);
patientId.value = decodeRouteParam(options.patientId);
patientName.value = decodeRouteParam(options.patientName);
teamId.value = decodeRouteParam(options.teamId);
teamName.value = decodeRouteParam(options.teamName);
corpId.value = decodeRouteParam(options.corpId) || accountStore.account?.corpId || "";
entries.value = [createEntry()];
loadTeamContext().then(() => {
loadCorpSettings();
loadStaffList();
});
});
async function loadTeamContext() {
if (!teamId.value || !corpId.value) return;
try {
const res = await api("getTeamData", { teamId: teamId.value, corpId: corpId.value }, false);
const team = res?.data || {};
if (team.corpId && team.corpId !== corpId.value) corpId.value = team.corpId;
} catch (error) {
console.error("加载服务团队失败", error);
}
}
async function loadStaffList() {
if (!corpId.value) return;
try {
const res = await api("getCorpMember", { corpId: corpId.value, page: 1, pageSize: 1000 }, false);
const list = res?.data?.data || res?.data?.list || res?.data || [];
staffList.value = Array.isArray(list)
? list.filter((staff) => staff?.userid).map((staff) => ({ ...staff, anotherName: staff.anotherName || staff.name || staff.userid }))
: [];
} catch (error) {
staffList.value = [];
console.error("加载机构员工失败", error);
}
}
async function loadCorpSettings() {
if (!corpId.value) return;
try {
const res = await api("getCorpInfo", { corpId: corpId.value }, false);
const corpInfo = res?.data || {};
showBenefitAmount.value = corpInfo.showBenefitAmount !== false;
} catch (error) {
showBenefitAmount.value = true;
console.error("加载机构权益配置失败", error);
}
}
function openProjectPicker(index) {
projectPickerIndex.value = index;
projectPickerVisible.value = true;
projectKeyword.value = "";
loadProjects({ reset: true });
}
function closeProjectPicker() {
projectPickerVisible.value = false;
projectKeyword.value = "";
}
function searchProjects() {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => loadProjects({ reset: true }), 300);
}
function normalizeProject(project = {}) {
const depts = Array.isArray(project.depts) ? project.depts : [];
return {
...project,
depts,
deptNames: depts.map((dept) => dept?.deptName).filter(Boolean).join("、"),
};
}
async function loadProjects({ reset = false } = {}) {
if (projectLoading.value) return;
if (!reset && !projectHasMore.value) return;
const page = reset ? 1 : projectPage.value;
projectLoading.value = true;
if (reset) {
projectList.value = [];
projectHasMore.value = true;
}
try {
const res = await api("getProjectList", {
corpId: corpId.value,
page,
pageSize: projectPageSize,
projectName: projectKeyword.value.trim(),
projectStatus: "enable",
showDepts: true,
}, false);
const list = res?.data?.list || res?.list || res?.data || [];
const nextList = Array.isArray(list) ? list.map(normalizeProject) : [];
projectList.value = reset ? nextList : projectList.value.concat(nextList);
projectPage.value = page + 1;
projectHasMore.value = nextList.length >= projectPageSize;
} catch (error) {
if (reset) projectList.value = [];
projectHasMore.value = false;
uni.showToast({ title: "项目加载失败", icon: "none" });
} finally {
projectLoading.value = false;
}
}
function loadMoreProjects() {
loadProjects();
}
function toNumber(value, fallback = 0) {
const number = Number(value);
return Number.isFinite(number) ? number : fallback;
}
function toFixedNumber(value, precision = 2) {
return Number(toNumber(value).toFixed(precision));
}
function clampNumber(value, min, max) {
return Math.min(max, Math.max(min, value));
}
function calculateTotalPrice(entry) {
if (entry.isFree) {
entry.discount = 0;
entry.totalPrice = 0;
return;
}
const total = toNumber(entry.price) * toNumber(entry.usageCount, 1) * (toNumber(entry.discount) / 10);
entry.totalPrice = toFixedNumber(Math.max(0, total), 2);
}
function calculateDiscount(entry) {
if (entry.isFree) {
entry.discount = 0;
entry.totalPrice = 0;
return;
}
const originalTotal = toNumber(entry.price) * toNumber(entry.usageCount);
entry.discount = originalTotal > 0
? toFixedNumber(clampNumber((toNumber(entry.totalPrice) / originalTotal) * 10, 0, 10), 1)
: 10;
}
function handleFreeChange(event, entry) {
entry.isFree = Array.isArray(event.detail.value) && event.detail.value.includes("free");
if (entry.isFree) {
entry.discount = 0;
entry.totalPrice = 0;
return;
}
entry.discount = 10;
calculateTotalPrice(entry);
}
function getProjectDepts(entry) {
return Array.isArray(entry?.project?.depts) ? entry.project.depts : [];
}
function getTreatmentDoctors(entry) {
if (!entry?.dept?._id) return [];
const allowedJobs = ["doctor", "expertDoctor", "therapist"];
const deptId = String(entry.dept._id);
return staffList.value.filter((staff) => {
const hasAllowedJob = Array.isArray(staff.job) && staff.job.some((job) => allowedJobs.includes(job));
const inDept = Array.isArray(staff.deptIds) && staff.deptIds.map(String).includes(deptId);
return hasAllowedJob && inDept;
});
}
function selectProject(project) {
const entry = entries.value[projectPickerIndex.value];
if (!entry) return;
const normalizedProject = normalizeProject(project);
entry.project = normalizedProject;
entry.price = Number(normalizedProject.price || 0);
entry.discount = 10;
entry.isFree = false;
calculateTotalPrice(entry);
entry.dept = getProjectDepts(entry).length === 1 ? getProjectDepts(entry)[0] : null;
entry.treatmentDoctorUserId = "";
entry.treatmentDoctorName = "";
closeProjectPicker();
}
function changeDept(event, entry) {
entry.dept = getProjectDepts(entry)[Number(event.detail.value)] || null;
entry.treatmentDoctorUserId = "";
entry.treatmentDoctorName = "";
}
function changeTreatmentDoctor(event, entry) {
const doctor = getTreatmentDoctors(entry)[Number(event.detail.value)] || null;
entry.treatmentDoctorUserId = doctor?.userid || "";
entry.treatmentDoctorName = doctor?.anotherName || doctor?.name || "";
}
function changeValidTime(event, entry) {
const [yearIndex, monthIndex, dayIndex] = event.detail.value || validTimeIndex.value;
const year = Number(validTimeRange.value[0][yearIndex].replace("年", ""));
const month = Number(validTimeRange.value[1][monthIndex].replace("月", ""));
const day = Number(validTimeRange.value[2][dayIndex].replace("日", ""));
const selectedDate = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
if (selectedDate < formatDate(new Date())) {
uni.showToast({
title: "不能选择过去的日期",
icon: "none",
duration: 2000,
});
return;
}
entry.validTime = selectedDate;
validTimeIndex.value = [yearIndex, monthIndex, dayIndex];
}
function clearValidTime(entry) {
entry.validTime = "";
}
function addEntry() {
const entry = createEntry();
entries.value.push(entry);
nextTick(() => {
pageRef.value?.scrollToBottom?.();
});
}
function removeEntry(index) {
entries.value.splice(index, 1);
if (!entries.value.length) entries.value.push(createEntry());
}
function goBack() {
uni.navigateBack();
}
function validateEntries() {
for (let index = 0; index < entries.value.length; index += 1) {
const entry = entries.value[index];
const label = `${index + 1}个权益项目`;
if (!entry.project?._id) return `${label}请选择项目`;
if (Number(entry.price) < 0 || Number.isNaN(Number(entry.price))) return `${label}价格不能小于0`;
if (!Number.isFinite(Number(entry.discount)) || Number(entry.discount) < 0 || Number(entry.discount) > 10) return `${label}折扣需在0到10之间`;
if (!Number.isFinite(Number(entry.totalPrice)) || Number(entry.totalPrice) < 0) return `${label}总价不能小于0`;
if (!Number(entry.usageCount) || Number(entry.usageCount) <= 0) return `${label}数量必须大于0`;
if (!entry.dept?._id) return `${label}请选择治疗科室`;
}
if (!patientId.value) return "当前会话缺少患者档案关联";
return "";
}
function buildBenefitMessage(createdEntries) {
const items = createdEntries.map((entry) => ({
projectId: entry.project._id,
projectName: entry.project.projectName,
usageCount: Number(entry.usageCount || 0),
validTime: entry.validTime ? new Date(`${entry.validTime} 23:59:59`).getTime() : null,
validTimeText: entry.validTime || "无限期",
}));
return {
conversationID: groupId.value ? `GROUP${groupId.value}` : "",
groupId: groupId.value,
createdAt: Date.now(),
messages: [
{
type: "benefit",
content: {
title: "医生为您录入专属权益",
patientId: patientId.value,
patientName: patientName.value,
corpId: corpId.value,
teamId: teamId.value,
items,
},
},
],
context: {
userId: accountStore.doctorInfo?.userid || accountStore.account?.userid || "",
customerId: patientId.value,
customerName: patientName.value,
corpId: corpId.value,
teamId: teamId.value,
},
};
}
function queueBenefitMessage(createdEntries) {
if (!groupId.value || !createdEntries.length) return;
uni.setStorageSync(PENDING_FOLLOWUP_SEND_STORAGE_KEY, buildBenefitMessage(createdEntries));
}
async function submitEntries() {
if (saving.value) return;
const errorMessage = validateEntries();
if (errorMessage) {
uni.showToast({ title: errorMessage, icon: "none" });
return;
}
saving.value = true;
const userId = accountStore.doctorInfo?.userid || accountStore.account?.userid || "";
try {
const createdEntries = [];
for (const entry of entries.value) {
const count = Number(entry.usageCount);
const price = Number(entry.price);
const discount = Number(entry.discount);
const totalPrice = entry.isFree ? 0 : Number(entry.totalPrice || 0);
const treatmentData = {
customerId: patientId.value,
customerName: patientName.value,
projectId: entry.project._id,
projectName: entry.project.projectName,
usageCount: count,
restUsageCount: count,
price,
totalPrice,
discount,
isFree: Boolean(entry.isFree),
treatmentDeptName: entry.dept.deptName,
treatmentDeptId: entry.dept._id,
treatmentDept_id: entry.dept._id,
treatmentDoctorUserId: entry.treatmentDoctorUserId,
treatmentStatus: "init",
deductUsageCount: 0,
billType: "门诊",
billdCreator: userId,
createTreatementTime: Date.now(),
billTime: Date.now(),
validTime: entry.validTime ? new Date(`${entry.validTime} 23:59:59`).getTime() : null,
teamId: teamId.value,
corpId: corpId.value,
};
const res = await api("addTreatmentRecord", { params: treatmentData }, false);
if (!res?.success) throw new Error(res?.message || "权益录入失败");
createdEntries.push(entry);
}
queueBenefitMessage(createdEntries);
uni.showToast({ title: "权益录入成功", icon: "success" });
setTimeout(goBack, 500);
} catch (error) {
uni.showToast({ title: error.message || "权益录入失败", icon: "none" });
} finally {
saving.value = false;
}
}
</script>
<style scoped lang="scss">
.benefit-page-shell {
background: #f5f6f8;
}
.benefit-page {
min-height: 100%;
padding: 20rpx 24rpx 32rpx;
box-sizing: border-box;
}
.patient-card {
position: relative;
display: flex;
align-items: stretch;
background: #fff;
border: 1rpx solid #e5e7eb;
border-radius: 20rpx;
padding: 24rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 10rpx 28rpx rgba(15, 23, 42, 0.06);
}
.patient-card-accent {
width: 8rpx;
border-radius: 999rpx;
background: #d1d5db;
margin-right: 18rpx;
flex-shrink: 0;
}
.patient-card-main {
flex: 1;
min-width: 0;
}
.patient-card-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16rpx;
}
.patient-name-wrap {
min-width: 0;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12rpx;
}
.patient-name {
color: #1f2937;
font-size: 36rpx;
font-weight: 700;
line-height: 1.2;
max-width: 420rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.patient-badge {
display: inline-flex;
align-items: center;
height: 36rpx;
padding: 0 14rpx;
border-radius: 999rpx;
background: #f3f4f6;
color: #6b7280;
font-size: 22rpx;
}
.patient-status {
flex-shrink: 0;
height: 36rpx;
line-height: 36rpx;
padding: 0 12rpx;
border-radius: 999rpx;
background: #f3f4f6;
color: #6b7280;
font-size: 22rpx;
}
.patient-meta {
display: block;
margin-top: 20rpx;
}
.patient-meta-item {
min-width: 0;
padding: 14rpx 16rpx;
border-radius: 12rpx;
background: #f8f9fb;
}
.patient-meta-label {
display: block;
color: #9ca3af;
font-size: 20rpx;
line-height: 1.2;
}
.patient-meta-value {
display: block;
margin-top: 6rpx;
color: #6b7280;
font-size: 24rpx;
font-weight: 500;
line-height: 1.35;
word-break: break-all;
}
.section-title {
margin: 12rpx 4rpx 16rpx;
color: #1f2937;
font-size: 28rpx;
font-weight: 700;
}
.entry-card {
background: #fff;
border-radius: 18rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 10rpx 28rpx rgba(15, 23, 42, 0.05);
}
.field-label {
margin-bottom: 12rpx;
color: #374151;
font-size: 26rpx;
font-weight: 600;
}
.project-input,
.picker-value {
min-height: 76rpx;
box-sizing: border-box;
border: 1rpx solid #d1d5db;
border-radius: 12rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
background: #fff;
color: #111827;
}
.number-input {
display: block;
width: 100%;
height: 76rpx;
min-width: 0;
box-sizing: border-box;
margin: 0;
padding: 0 20rpx;
border: 1rpx solid #b8c1cc;
border-radius: 12rpx;
background: #fff;
color: #1f2937;
-webkit-text-fill-color: #1f2937;
caret-color: #0877f1;
font-size: 28rpx;
font-weight: 600;
line-height: 76rpx;
opacity: 1;
text-align: left;
}
.locked-input {
background: #f3f4f6;
color: #9ca3af;
}
.value-text {
color: #111827;
font-size: 28rpx;
font-weight: 500;
}
.placeholder-text {
color: #9ca3af;
font-size: 28rpx;
}
.field-row {
display: flex;
gap: 20rpx;
margin-top: 20rpx;
}
.field-group {
margin-top: 20rpx;
}
.field-block {
flex: 1;
min-width: 0;
}
.field-block-full {
flex-basis: 100%;
}
.discount-input-wrap {
display: flex;
align-items: center;
gap: 12rpx;
}
.discount-input-wrap .number-input {
flex: 1;
min-width: 0;
}
.free-check {
flex-shrink: 0;
}
.free-check-label {
display: flex;
align-items: center;
gap: 4rpx;
color: #374151;
font-size: 24rpx;
}
.valid-time-wrap {
display: flex;
align-items: center;
gap: 12rpx;
}
.valid-time-picker {
flex: 1;
min-width: 0;
}
.unlimited-button {
flex-shrink: 0;
height: 76rpx;
line-height: 76rpx;
padding: 0 20rpx;
border: 1rpx solid #d1d5db;
border-radius: 12rpx;
color: #0877f1;
font-size: 26rpx;
background: #f8fbff;
}
.total-price {
min-height: 40rpx;
color: #e85c2f;
font-size: 30rpx;
font-weight: 600;
line-height: 40rpx;
}
.number-input {
overflow: visible;
}
.number-input[disabled] {
color: #374151;
-webkit-text-fill-color: #374151;
background: #f3f4f6;
}
.total-input {
color: #c2410c;
font-weight: 600;
}
.add-entry {
height: 80rpx;
border: 1rpx dashed #0877f1;
border-radius: 14rpx;
display: flex;
justify-content: center;
align-items: center;
gap: 8rpx;
color: #0877f1;
font-size: 28rpx;
background: #fff;
margin-top: 8rpx;
}
.remove-entry {
margin-top: 20rpx;
color: #fa5151;
font-size: 26rpx;
text-align: right;
}
.footer-bar {
display: flex;
gap: 20rpx;
padding: 20rpx 24rpx;
background: rgba(245, 246, 248, 0.96);
border-top: 1rpx solid #eef2f7;
backdrop-filter: blur(12px);
box-sizing: border-box;
}
.footer-bar button {
flex: 1;
min-height: 88rpx;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 14rpx;
font-size: 30rpx;
line-height: 1.2;
}
.cancel-button {
color: #0877f1;
background: #fff;
border: 1rpx solid #0877f1;
}
.submit-button {
color: #fff;
background: linear-gradient(135deg, #0f84ff 0%, #0877f1 100%);
box-shadow: 0 10rpx 24rpx rgba(8, 119, 241, 0.22);
}
.picker-mask {
position: fixed;
inset: 0;
z-index: 20;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: flex-end;
}
.project-picker {
width: 100%;
max-height: 75vh;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
padding: 24rpx;
box-sizing: border-box;
}
.picker-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-title {
color: #222;
font-size: 32rpx;
font-weight: 600;
}
.search-box {
display: flex;
align-items: center;
gap: 12rpx;
background: #f5f6f8;
border-radius: 10rpx;
padding: 0 20rpx;
margin: 20rpx 0;
}
.search-input {
flex: 1;
height: 72rpx;
font-size: 28rpx;
}
.project-list {
max-height: 55vh;
}
.project-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 20rpx;
padding: 24rpx 8rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.project-info {
flex: 1;
min-width: 0;
}
.project-name {
color: #333;
font-size: 28rpx;
line-height: 1.35;
}
.project-depts {
margin-top: 8rpx;
color: #8b95a1;
font-size: 24rpx;
line-height: 1.35;
word-break: break-all;
}
.project-price {
color: #fa5151;
font-size: 26rpx;
}
.empty-text {
text-align: center;
color: #999;
padding: 60rpx 0;
font-size: 28rpx;
}
</style>