管理后台前后端逻辑、UI美化

This commit is contained in:
Cheng Zhou
2026-01-16 13:50:08 +08:00
parent 05c1f9579a
commit 7fdcfdaadd
82 changed files with 3210 additions and 549 deletions
+18 -2
View File
@@ -6,7 +6,7 @@
<p class="page-subtitle">{{ TEXT.modules.feeContracts.detailSubtitle }}</p>
</div>
<div class="actions">
<el-button type="primary" :disabled="!canWrite" @click="goEdit" class="header-action-btn copy-btn">
<el-button type="primary" :disabled="!canWrite || isReadOnly" @click="goEdit" class="header-action-btn copy-btn">
<el-icon class="el-icon--left"><Edit /></el-icon>
{{ TEXT.common.actions.edit }}
</el-button>
@@ -106,6 +106,7 @@
:entity-type="'contract_fee'"
:entity-id="contractId"
:groups="attachmentGroups"
:readonly="isReadOnly"
/>
</el-card>
</template>
@@ -115,6 +116,7 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { Edit } from "@element-plus/icons-vue";
import { getContractFee } from "../../api/feeContracts";
import { fetchSites } from "../../api/sites";
@@ -145,6 +147,14 @@ const detail = reactive<any>({
payments: [],
});
const sites = ref<any[]>([]);
const siteActiveMap = computed(() => {
const map: Record<string, boolean> = {};
sites.value.forEach((site) => {
if (site?.id) map[site.id] = !!site.is_active;
});
return map;
});
const isReadOnly = computed(() => !!detail.center_id && siteActiveMap.value[detail.center_id] === false);
const attachmentGroups = [
{
@@ -204,7 +214,13 @@ const formatAmountWan = (value: any) => {
return (numberValue / 10000).toFixed(2);
};
const goEdit = () => router.push(`/fees/contracts/${contractId}/edit`);
const goEdit = () => {
if (isReadOnly.value) {
ElMessage.warning("中心已停用");
return;
}
router.push(`/fees/contracts/${contractId}/edit`);
};
const goBack = () => router.push("/fees/contracts");
onMounted(async () => {
+72 -13
View File
@@ -29,39 +29,69 @@
<el-row :gutter="24">
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.site" prop="center_id" required>
<el-select v-model="form.center_id" :disabled="isEdit" :placeholder="TEXT.common.placeholders.select" class="full-width">
<el-select
v-model="form.center_id"
:disabled="isEdit || isReadOnly"
:placeholder="TEXT.common.placeholders.select"
class="full-width"
>
<el-option
v-for="site in sites"
:key="site.id"
:label="site.name || TEXT.common.fallback"
:value="site.id"
:disabled="!site.is_active"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.modules.feeContracts.contractAmount" prop="contract_amount" required>
<el-input-number v-model="form.contract_amount" :min="0" :precision="2" :step="1000" class="full-width" controls-position="right" />
<el-input-number
v-model="form.contract_amount"
:disabled="isReadOnly"
:min="0"
:precision="2"
:step="1000"
class="full-width"
controls-position="right"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.modules.feeContracts.contractCases" prop="contract_cases" required>
<el-input-number v-model="form.contract_cases" :min="0" :step="1" class="full-width" controls-position="right" />
<el-input-number v-model="form.contract_cases" :disabled="isReadOnly" :min="0" :step="1" class="full-width" controls-position="right" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.modules.feeContracts.actualCases" prop="actual_cases">
<el-input-number v-model="form.actual_cases" :min="0" :step="1" class="full-width" controls-position="right" />
<el-input-number v-model="form.actual_cases" :disabled="isReadOnly" :min="0" :step="1" class="full-width" controls-position="right" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.modules.feeContracts.settlementAmount" prop="settlement_amount">
<el-input-number v-model="form.settlement_amount" :min="0" :precision="2" :step="1000" class="full-width" controls-position="right" />
<el-input-number
v-model="form.settlement_amount"
:disabled="isReadOnly"
:min="0"
:precision="2"
:step="1000"
class="full-width"
controls-position="right"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.modules.feeContracts.finalPaymentAmount" prop="final_payment_amount">
<el-input-number v-model="form.final_payment_amount" :min="0" :precision="2" :step="1000" class="full-width" controls-position="right" />
<el-input-number
v-model="form.final_payment_amount"
:disabled="isReadOnly"
:min="0"
:precision="2"
:step="1000"
class="full-width"
controls-position="right"
/>
</el-form-item>
</el-col>
</el-row>
@@ -71,7 +101,7 @@
<template #header>
<div class="card-header actions-header">
<span class="header-title">{{ TEXT.modules.feeContracts.paymentTitle }}</span>
<el-button type="primary" @click="addPayment">
<el-button type="primary" :disabled="isReadOnly" @click="addPayment">
<el-icon class="el-icon--left"><Plus /></el-icon>
{{ TEXT.common.actions.add }}
</el-button>
@@ -90,7 +120,7 @@
<span class="seq-circle">{{ index + 1 }}</span>
<span class="seq-text">{{ TEXT.modules.feeContracts.paymentSeqUnit }}</span>
</div>
<el-button link type="danger" @click="removePayment(index)">
<el-button link type="danger" :disabled="isReadOnly" @click="removePayment(index)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
@@ -98,13 +128,13 @@
<el-row :gutter="16">
<el-col :span="8">
<el-form-item :label="TEXT.common.fields.amount" :error="paymentErrors[index]?.amount">
<el-input-number v-model="payment.amount" :min="0" :precision="2" class="full-width" controls-position="right" />
<el-input-number v-model="payment.amount" :disabled="isReadOnly" :min="0" :precision="2" class="full-width" controls-position="right" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="TEXT.modules.feeContracts.paidFlag" :error="paymentErrors[index]?.paid_date">
<div class="status-control">
<el-checkbox v-model="payment.is_paid" @change="() => onPaidToggle(payment)">
<el-checkbox v-model="payment.is_paid" :disabled="isReadOnly" @change="() => onPaidToggle(payment)">
{{ TEXT.modules.feeContracts.isPaid }}
</el-checkbox>
<el-date-picker
@@ -113,6 +143,7 @@
type="date"
value-format="YYYY-MM-DD"
:placeholder="TEXT.common.placeholders.select"
:disabled="isReadOnly"
class="status-picker"
size="small"
/>
@@ -122,7 +153,7 @@
<el-col :span="8">
<el-form-item :label="TEXT.modules.feeContracts.verifiedFlag" :error="paymentErrors[index]?.verified_date">
<div class="status-control">
<el-checkbox v-model="payment.is_verified" @change="() => onVerifiedToggle(payment)">
<el-checkbox v-model="payment.is_verified" :disabled="isReadOnly" @change="() => onVerifiedToggle(payment)">
{{ TEXT.modules.feeContracts.isVerified }}
</el-checkbox>
<el-date-picker
@@ -131,6 +162,7 @@
type="date"
value-format="YYYY-MM-DD"
:placeholder="TEXT.common.placeholders.select"
:disabled="isReadOnly"
class="status-picker"
size="small"
/>
@@ -140,7 +172,7 @@
</el-row>
<el-form-item :label="TEXT.common.fields.remark" class="mb-0">
<el-input v-model="payment.remark" type="textarea" :rows="2" :placeholder="TEXT.common.placeholders.input" />
<el-input v-model="payment.remark" :disabled="isReadOnly" type="textarea" :rows="2" :placeholder="TEXT.common.placeholders.input" />
</el-form-item>
<div v-if="paymentErrors[index]?.verification" class="payment-error">
@@ -162,6 +194,7 @@
:entity-type="'contract_fee'"
:entity-id="form.id"
:groups="attachmentGroups"
:readonly="isReadOnly"
/>
<StateEmpty v-else :description="TEXT.modules.feeContracts.uploadHint" class="compact-empty" />
@@ -169,7 +202,7 @@
<div class="footer-actions">
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
<el-button type="primary" :loading="saving" @click="submit" class="save-btn">
<el-button type="primary" :loading="saving" :disabled="isReadOnly" @click="submit" class="save-btn">
{{ TEXT.common.actions.save }}
</el-button>
</div>
@@ -207,6 +240,14 @@ const loading = ref(false);
const saving = ref(false);
const errorMessage = ref("");
const sites = ref<any[]>([]);
const siteActiveMap = computed(() => {
const map: Record<string, boolean> = {};
sites.value.forEach((site) => {
if (site?.id) map[site.id] = !!site.is_active;
});
return map;
});
const isReadOnly = computed(() => isEdit.value && !!form.center_id && siteActiveMap.value[form.center_id] === false);
const formRef = ref();
const form = reactive({
@@ -302,6 +343,10 @@ const loadDetail = async () => {
};
const addPayment = () => {
if (isReadOnly.value) {
ElMessage.warning("中心已停用");
return;
}
payments.value.push({
tempId: `${Date.now()}-${Math.random()}`,
amount: 0,
@@ -314,6 +359,10 @@ const addPayment = () => {
};
const removePayment = (index: number) => {
if (isReadOnly.value) {
ElMessage.warning("中心已停用");
return;
}
const payment = payments.value[index];
if (payment?.id) {
removedPaymentIds.value.push(payment.id);
@@ -322,6 +371,7 @@ const removePayment = (index: number) => {
};
const onPaidToggle = (payment: any) => {
if (isReadOnly.value) return;
if (!payment.is_paid) {
payment.paid_date = "";
payment.is_verified = false;
@@ -330,6 +380,7 @@ const onPaidToggle = (payment: any) => {
};
const onVerifiedToggle = (payment: any) => {
if (isReadOnly.value) return;
if (payment.is_verified && !payment.is_paid) {
payment.is_paid = true;
}
@@ -367,6 +418,14 @@ const validatePayments = () => {
const submit = async () => {
if (!study.currentStudy?.id) return;
if (isReadOnly.value) {
ElMessage.warning("中心已停用");
return;
}
if (form.center_id && siteActiveMap.value[form.center_id] === false) {
ElMessage.warning("中心已停用");
return;
}
const formOk = await formRef.value?.validate?.().catch(() => false);
if (!formOk) return;
if (!validatePayments()) {
+38 -4
View File
@@ -3,7 +3,6 @@
<div class="page-header">
<div>
<h1 class="page-title">{{ TEXT.modules.feeContracts.title }}</h1>
<p class="page-subtitle">{{ TEXT.modules.feeContracts.subtitle }}</p>
</div>
<el-button type="primary" :disabled="!canWrite" @click="goNew" class="header-action-btn">
<el-icon class="el-icon--left"><Plus /></el-icon>
@@ -94,7 +93,13 @@
<StateLoading v-else-if="loading" :rows="6" />
<el-card v-else class="table-card" shadow="never">
<el-table :data="contracts" style="width: 100%" @row-click="onRowClick" :header-cell-style="{ background: '#f8f9fb' }">
<el-table
:data="sortedContracts"
style="width: 100%"
@row-click="onRowClick"
:row-class-name="contractRowClass"
:header-cell-style="{ background: '#f8f9fb' }"
>
<el-table-column prop="center_name" :label="TEXT.common.fields.site" min-width="180">
<template #default="scope">
<div class="site-cell">
@@ -163,7 +168,7 @@
link
type="danger"
size="small"
:disabled="!canWrite"
:disabled="!canWrite || isInactiveSite(scope.row.center_id)"
@click.stop="remove(scope.row)"
>
{{ TEXT.common.actions.delete }}
@@ -171,7 +176,7 @@
</template>
</el-table-column>
</el-table>
<StateEmpty v-if="contracts.length === 0" :description="TEXT.modules.feeContracts.empty" />
<StateEmpty v-if="sortedContracts.length === 0" :description="TEXT.modules.feeContracts.empty" />
</el-card>
</div>
</template>
@@ -201,6 +206,19 @@ const loading = ref(false);
const errorMessage = ref("");
const contracts = ref<any[]>([]);
const sites = ref<any[]>([]);
const siteActiveMap = computed(() => {
const map: Record<string, boolean> = {};
sites.value.forEach((site) => {
if (site?.id) map[site.id] = !!site.is_active;
});
return map;
});
const isInactiveSite = (siteId?: string) => !!siteId && siteActiveMap.value[siteId] === false;
const sortedContracts = computed(() =>
[...contracts.value].sort((a, b) => Number(isInactiveSite(a?.center_id)) - Number(isInactiveSite(b?.center_id)))
);
const contractRowClass = ({ row }: { row: any }) =>
`${row?.id ? "clickable-row" : ""}${isInactiveSite(row?.center_id) ? " row-inactive" : ""}`.trim();
const filters = reactive({
centerId: "",
@@ -279,6 +297,10 @@ const onRowClick = (row: any) => {
const remove = async (row: any) => {
if (!row?.id || !canWrite.value) return;
if (isInactiveSite(row?.center_id)) {
ElMessage.warning("中心已停用");
return;
}
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
if (!ok) return;
try {
@@ -438,4 +460,16 @@ onMounted(async () => {
color: var(--ctms-text-regular);
font-family: var(--el-font-family);
}
</style>
<style>
.row-inactive td {
color: var(--ctms-text-secondary);
background-color: #fff7d6 !important;
}
.row-inactive .el-tag {
opacity: 0.6;
}
</style>
@@ -6,7 +6,7 @@
<p class="page-subtitle">{{ TEXT.modules.feeSpecials.detailSubtitle }}</p>
</div>
<div class="actions">
<el-button type="primary" :disabled="!canWrite" @click="goEdit" class="header-action-btn copy-btn">
<el-button type="primary" :disabled="!canWrite || isReadOnly" @click="goEdit" class="header-action-btn copy-btn">
<el-icon class="el-icon--left"><Edit /></el-icon>
{{ TEXT.common.actions.edit }}
</el-button>
@@ -82,6 +82,7 @@
:entity-type="'special_expense'"
:entity-id="expenseId"
:groups="attachmentGroups"
:readonly="isReadOnly"
/>
</el-card>
</template>
@@ -91,6 +92,7 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { Edit } from "@element-plus/icons-vue";
import { getSpecialExpense } from "../../api/feeSpecials";
import { fetchSites } from "../../api/sites";
@@ -123,6 +125,14 @@ const detail = reactive<any>({
verified_date: "",
});
const sites = ref<any[]>([]);
const siteActiveMap = computed(() => {
const map: Record<string, boolean> = {};
sites.value.forEach((site) => {
if (site?.id) map[site.id] = !!site.is_active;
});
return map;
});
const isReadOnly = computed(() => !!detail.center_id && siteActiveMap.value[detail.center_id] === false);
const attachmentGroups = [
{
@@ -188,7 +198,13 @@ const getCategoryType = (category: string) => {
return 'info';
};
const goEdit = () => router.push(`/fees/special/${expenseId}/edit`);
const goEdit = () => {
if (isReadOnly.value) {
ElMessage.warning("中心已停用");
return;
}
router.push(`/fees/special/${expenseId}/edit`);
};
const goBack = () => router.push("/fees/special");
onMounted(async () => {
+28 -10
View File
@@ -29,36 +29,37 @@
<el-row :gutter="24">
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.site">
<el-select v-model="form.center_id" clearable :placeholder="TEXT.common.placeholders.select" class="full-width">
<el-select v-model="form.center_id" clearable :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.select" class="full-width">
<el-option
v-for="site in sites"
:key="site.id"
:label="site.name || TEXT.common.fallback"
:value="site.id"
:disabled="!site.is_active"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.occurDate" prop="happen_date">
<el-date-picker v-model="form.happen_date" type="date" value-format="YYYY-MM-DD" class="full-width" />
<el-date-picker v-model="form.happen_date" :disabled="isReadOnly" type="date" value-format="YYYY-MM-DD" class="full-width" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.category" prop="category" required>
<el-select v-model="form.category" :placeholder="TEXT.common.placeholders.select" class="full-width">
<el-select v-model="form.category" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.select" class="full-width">
<el-option v-for="option in categoryOptions" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.amount" prop="amount" required>
<el-input-number v-model="form.amount" :min="0" :precision="2" class="full-width" controls-position="right" />
<el-input-number v-model="form.amount" :disabled="isReadOnly" :min="0" :precision="2" class="full-width" controls-position="right" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item :label="TEXT.common.fields.description">
<el-input v-model="form.description" type="textarea" :rows="3" />
<el-input v-model="form.description" :disabled="isReadOnly" type="textarea" :rows="3" />
</el-form-item>
</el-col>
</el-row>
@@ -76,7 +77,7 @@
<div class="status-box">
<div class="status-header">
<span class="status-title">{{ TEXT.modules.feeSpecials.paidFlag }}</span>
<el-switch v-model="form.is_paid" @change="onPaidToggle" />
<el-switch v-model="form.is_paid" :disabled="isReadOnly" @change="onPaidToggle" />
</div>
<el-form-item :label="TEXT.common.fields.occurDate" :error="specialErrors.paid_date" class="status-form-item">
<el-date-picker
@@ -84,7 +85,7 @@
type="date"
value-format="YYYY-MM-DD"
:placeholder="TEXT.common.placeholders.select"
:disabled="!form.is_paid"
:disabled="isReadOnly || !form.is_paid"
class="full-width"
/>
</el-form-item>
@@ -94,7 +95,7 @@
<div class="status-box">
<div class="status-header">
<span class="status-title">{{ TEXT.modules.feeSpecials.verifiedFlag }}</span>
<el-switch v-model="form.is_verified" @change="onVerifiedToggle" />
<el-switch v-model="form.is_verified" :disabled="isReadOnly" @change="onVerifiedToggle" />
</div>
<el-form-item :label="TEXT.common.fields.occurDate" :error="specialErrors.verified_date" class="status-form-item">
<el-date-picker
@@ -102,7 +103,7 @@
type="date"
value-format="YYYY-MM-DD"
:placeholder="TEXT.common.placeholders.select"
:disabled="!form.is_verified"
:disabled="isReadOnly || !form.is_verified"
class="full-width"
/>
</el-form-item>
@@ -126,6 +127,7 @@
:entity-type="'special_expense'"
:entity-id="form.id"
:groups="attachmentGroups"
:readonly="isReadOnly"
/>
<StateEmpty v-else :description="TEXT.modules.feeSpecials.uploadHint" class="compact-empty" />
@@ -133,7 +135,7 @@
<div class="footer-actions">
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
<el-button type="primary" :loading="saving" @click="submit" class="save-btn">
<el-button type="primary" :loading="saving" :disabled="isReadOnly" @click="submit" class="save-btn">
{{ TEXT.common.actions.save }}
</el-button>
</div>
@@ -163,6 +165,14 @@ const loading = ref(false);
const saving = ref(false);
const errorMessage = ref("");
const sites = ref<any[]>([]);
const siteActiveMap = computed(() => {
const map: Record<string, boolean> = {};
sites.value.forEach((site) => {
if (site?.id) map[site.id] = !!site.is_active;
});
return map;
});
const isReadOnly = computed(() => isEdit.value && !!form.center_id && siteActiveMap.value[form.center_id] === false);
const formRef = ref();
const form = reactive({
@@ -301,6 +311,14 @@ const validateSpecial = () => {
const submit = async () => {
if (!study.currentStudy?.id) return;
if (isReadOnly.value) {
ElMessage.warning("中心已停用");
return;
}
if (form.center_id && siteActiveMap.value[form.center_id] === false) {
ElMessage.warning("中心已停用");
return;
}
const formOk = await formRef.value?.validate?.().catch(() => false);
if (!formOk) return;
if (!validateSpecial()) {
+38 -4
View File
@@ -3,7 +3,6 @@
<div class="page-header">
<div>
<h1 class="page-title">{{ TEXT.modules.feeSpecials.title }}</h1>
<p class="page-subtitle">{{ TEXT.modules.feeSpecials.subtitle }}</p>
</div>
<el-button type="primary" :disabled="!canWrite" @click="goNew" class="header-action-btn">
<el-icon class="el-icon--left"><Plus /></el-icon>
@@ -109,7 +108,13 @@
<StateLoading v-else-if="loading" :rows="6" />
<el-card v-else class="table-card" shadow="never">
<el-table :data="expenses" style="width: 100%" @row-click="onRowClick" :header-cell-style="{ background: '#f8f9fb' }">
<el-table
:data="sortedExpenses"
style="width: 100%"
@row-click="onRowClick"
:header-cell-style="{ background: '#f8f9fb' }"
:row-class-name="expenseRowClass"
>
<el-table-column :label="TEXT.common.fields.occurDate" width="140">
<template #default="scope">
<span class="date-text">{{ displayDate(scope.row.happen_date) }}</span>
@@ -161,7 +166,7 @@
link
type="danger"
size="small"
:disabled="!canWrite"
:disabled="!canWrite || isInactiveSite(scope.row.center_id)"
@click.stop="remove(scope.row)"
>
{{ TEXT.common.actions.delete }}
@@ -169,7 +174,7 @@
</template>
</el-table-column>
</el-table>
<StateEmpty v-if="expenses.length === 0" :description="TEXT.modules.feeSpecials.empty" />
<StateEmpty v-if="sortedExpenses.length === 0" :description="TEXT.modules.feeSpecials.empty" />
</el-card>
</div>
</template>
@@ -199,6 +204,19 @@ const loading = ref(false);
const errorMessage = ref("");
const expenses = ref<any[]>([]);
const sites = ref<any[]>([]);
const siteActiveMap = computed(() => {
const map: Record<string, boolean> = {};
sites.value.forEach((site) => {
if (site?.id) map[site.id] = !!site.is_active;
});
return map;
});
const isInactiveSite = (siteId?: string) => !!siteId && siteActiveMap.value[siteId] === false;
const expenseRowClass = ({ row }: { row: any }) =>
`${row?.id ? "clickable-row" : ""}${isInactiveSite(row?.center_id) ? " row-inactive" : ""}`.trim();
const sortedExpenses = computed(() =>
[...expenses.value].sort((a, b) => Number(isInactiveSite(a?.center_id)) - Number(isInactiveSite(b?.center_id)))
);
const filters = reactive({
centerId: "",
@@ -287,6 +305,10 @@ const onRowClick = (row: any) => {
const remove = async (row: any) => {
if (!row?.id || !canWrite.value) return;
if (isInactiveSite(row?.center_id)) {
ElMessage.warning("中心已停用");
return;
}
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
if (!ok) return;
try {
@@ -444,4 +466,16 @@ onMounted(async () => {
.text-muted {
color: var(--ctms-text-placeholder);
}
</style>
<style>
.row-inactive td {
color: var(--ctms-text-secondary);
background-color: #fff7d6 !important;
}
.row-inactive .el-tag {
opacity: 0.6;
}
</style>