天津_建诚恒商贸
1.备件管理-新增独立备件出库功能入口,支持直接完成备件领用出库操作。
2.设备保养-支持按日期分组折叠展示,分组条目可展开查看明细;单条明细行末尾增加【保养完成】操作按钮,可单独办结单条保养任务;折叠汇总状态下,新增【一键保养完成】批量操作按钮,快速办结当前分组全部保养项。
3.设备巡检-按日期分组折叠展示,分组支持展开查看明细;每条巡检明细后增设【巡检完成】按钮,支持单独办结单条巡检记录;分组折叠未展开状态下,提供【一键巡检完成】批量操作,统一办结该分组下所有巡检任务。
4.增值税对比-优化数据展示逻辑,列表展示各订单增值税明细;页面右侧新增柱状可视化图表,直观对比进销项增值税数据。
5.管理驾驶舱-指标修改:将顶部「销售产品数」调整为「销售订单数」;时间筛选:页面左上角新增日期范围下拉筛选控件,所有统计指标标注当前筛选时段;
| | |
| | | }); |
| | | }; |
| | | |
| | | // 备件直接出库 |
| | | export function outboundSpareParts(data) { |
| | | return request({ |
| | | url: '/sparePartsRequisitionRecord/outbound', |
| | | method: 'post', |
| | | data |
| | | }) |
| | | } |
| | | |
| | |
| | | params: params, |
| | | }); |
| | | }; |
| | | // 单条保养完成 |
| | | export function completeMaintenance(id) { |
| | | return request({ |
| | | url: '/device/maintenance/complete/' + id, |
| | | method: 'put' |
| | | }) |
| | | } |
| | | |
| | | // 一键保养完成 |
| | | export function batchCompleteMaintenance(dateStr) { |
| | | return request({ |
| | | url: '/device/maintenance/batchComplete', |
| | | method: 'put', |
| | | params: { dateStr } |
| | | }) |
| | | } |
| | | |
| | | // 设备保养定时任务列表 |
| | | export const deviceMaintenanceTaskDel = (params) => { |
| | | return request({ |
| | |
| | | data: query |
| | | }) |
| | | } |
| | | |
| | | // 单条巡检完成 |
| | | export function completeInspection(id) { |
| | | return request({ |
| | | url: '/inspectionTask/complete/' + id, |
| | | method: 'put' |
| | | }) |
| | | } |
| | | |
| | | // 一键巡检完成 |
| | | export function batchCompleteInspection(dateStr) { |
| | | return request({ |
| | | url: '/inspectionTask/batchComplete', |
| | | method: 'put', |
| | | params: { dateStr } |
| | | }) |
| | | } |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // 分页查询 |
| | | // 分页查询(新增 type 参数) |
| | | export function getTaxList(query) { |
| | | return request({ |
| | | url: "/purchase/report/listVat", |
| | |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // 增值税进销项图表数据 |
| | | export function getVatChart(year) { |
| | | return request({ |
| | | url: '/purchase/report/vatChart', |
| | | method: 'get', |
| | | params: { year } |
| | | }) |
| | | } |
| | | |
| | | // 增值税明细 |
| | | export function getVatDetail(query) { |
| | | return request({ |
| | | url: '/purchase/report/listVatDetail', |
| | | method: 'get', |
| | | params: query |
| | | }) |
| | | } |
| | |
| | | }; |
| | | |
| | | // 供应商采购排名 |
| | | export const supplierPurchaseRanking = (query) => { |
| | | export const supplierPurchaseRanking = (params) => { |
| | | return request({ |
| | | url: "/home/supplierPurchaseRanking", |
| | | method: "get", |
| | | params: query, |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | | // 客户金额贡献排名 |
| | | export const customerContributionRanking = (query) => { |
| | | export const customerContributionRanking = (params) => { |
| | | return request({ |
| | | url: "/home/customerContributionRanking", |
| | | method: "get", |
| | | params: query, |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | |
| | | }; |
| | | |
| | | // 产品销售金额分析 |
| | | export const productSalesAnalysis = () => { |
| | | export const productSalesAnalysis = (params) => { |
| | | return request({ |
| | | url: "/home/productSalesAnalysis", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | |
| | | }; |
| | | |
| | | // 原材料采购金额占比 |
| | | export const rawMaterialPurchaseAmountRatio = () => { |
| | | export const rawMaterialPurchaseAmountRatio = (params) => { |
| | | return request({ |
| | | url: "/home/rawMaterialPurchaseAmountRatio", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | |
| | | }; |
| | | |
| | | // 销售/采购/储存产品数 |
| | | export const salesPurchaseStorageProductCount = () => { |
| | | export const salesPurchaseStorageProductCount = (startDate, endDate) => { |
| | | return request({ |
| | | url: "/home/salesPurchaseStorageProductCount", |
| | | method: "get", |
| | | params: { startDate, endDate }, |
| | | }); |
| | | }; |
| | | |
| | |
| | | }; |
| | | |
| | | // 产品周转天数 |
| | | export const productTurnoverDays = () => { |
| | | export const productTurnoverDays = (params) => { |
| | | return request({ |
| | | url: "/home/productTurnoverDays", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | |
| | | { prop: "registrant", label: "登记人", minWidth: 120 }, |
| | | { |
| | | prop: "createTime", label: "登记日期", minWidth: 180, |
| | | formatData: cell => { if (!cell) return "-"; try { return dayjs(cell).format("YYYY-MM-DD"); } catch { return cell; } }, |
| | | formatData: cell => { if (!cell) return "-"; try { return dayjs(cell).format("YYYY-MM-DD HH:mm:ss"); } catch { return cell; } }, |
| | | }, |
| | | { prop: "abnormalDescription", label: "异常描述", minWidth: 150 }, |
| | | ]); |
| | |
| | | <el-select v-model="usageQuery.sourceType" placeholder="请选择" clearable style="width: 200px"> |
| | | <el-option label="维修" :value="0" /> |
| | | <el-option label="保养" :value="1" /> |
| | | <el-option label="直接出库" :value="2" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item> |
| | |
| | | <el-button @click="resetUsageQuery">重置</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div> |
| | | <el-button type="primary" @click="openRequisitionDialog">直接出库</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="table_list"> |
| | | <PIMTable |
| | |
| | | </div> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | |
| | | <!-- 直接出库弹窗 --> |
| | | <el-dialog title="直接出库" v-model="requisitionDialogVisible" width="500px"> |
| | | <el-form :model="requisitionForm" :rules="requisitionRules" ref="requisitionFormRef" label-width="100px"> |
| | | <el-form-item label="设备选择" prop="deviceLedgerId"> |
| | | <el-select |
| | | v-model="requisitionForm.deviceLedgerId" |
| | | placeholder="请选择设备" |
| | | filterable |
| | | clearable |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="item in requisitionDeviceOptions" |
| | | :key="item.id" |
| | | :label="item.deviceName" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="备件选择" prop="sparePartsId"> |
| | | <el-select |
| | | v-model="requisitionForm.sparePartsId" |
| | | placeholder="请选择备件" |
| | | filterable |
| | | clearable |
| | | style="width: 100%" |
| | | @change="onRequisitionSparePartChange" |
| | | > |
| | | <el-option |
| | | v-for="item in requisitionSparePartsOptions" |
| | | :key="item.id" |
| | | :label="`${item.name}(库存: ${item.quantity})`" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="领用数量" prop="quantity"> |
| | | <el-input-number |
| | | v-model="requisitionForm.quantity" |
| | | :min="1" |
| | | :max="selectedSparePartMaxQty" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="备注" prop="remark"> |
| | | <el-input v-model="requisitionForm.remark" placeholder="请输入备注" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button type="primary" @click="submitRequisition" :loading="requisitionLoading">确定</el-button> |
| | | <el-button @click="requisitionDialogVisible = false">取消</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | import { getSparePartsList, addSparePart, editSparePart, delSparePart } from "@/api/equipmentManagement/spareParts"; |
| | | import { getDeviceLedger } from "@/api/equipmentManagement/ledger"; |
| | | import PIMTable from "@/components/PIMTable/PIMTable.vue"; |
| | | import { getSparePartsUsagePage } from "@/api/equipmentManagement/sparePartsUsage"; |
| | | import { getSparePartsUsagePage, outboundSpareParts } from "@/api/equipmentManagement/sparePartsUsage"; |
| | | |
| | | // 加载状态 |
| | | const loading = ref(false); |
| | |
| | | } |
| | | }; |
| | | |
| | | // 直接出库 |
| | | const requisitionDialogVisible = ref(false); |
| | | const requisitionLoading = ref(false); |
| | | const requisitionFormRef = ref(null); |
| | | const requisitionDeviceOptions = ref([]); |
| | | const requisitionSparePartsOptions = ref([]); |
| | | const selectedSparePartMaxQty = ref(1); |
| | | const requisitionForm = reactive({ |
| | | deviceLedgerId: null, |
| | | sparePartsId: null, |
| | | quantity: 1, |
| | | remark: '', |
| | | }); |
| | | const requisitionRules = { |
| | | sparePartsId: [{ required: true, message: '请选择备件', trigger: 'change' }], |
| | | quantity: [{ required: true, message: '请输入领用数量', trigger: 'blur' }], |
| | | }; |
| | | |
| | | const loadRequisitionOptions = async () => { |
| | | try { |
| | | const [deviceRes, spareRes] = await Promise.all([ |
| | | getDeviceLedger(), |
| | | getSparePartsList({ current: 1, size: 9999 }), |
| | | ]); |
| | | requisitionDeviceOptions.value = deviceRes.data || []; |
| | | requisitionSparePartsOptions.value = spareRes.data?.records || []; |
| | | } catch (e) { |
| | | ElMessage.error('加载选项失败'); |
| | | } |
| | | }; |
| | | |
| | | const openRequisitionDialog = async () => { |
| | | requisitionForm.deviceLedgerId = null; |
| | | requisitionForm.sparePartsId = null; |
| | | requisitionForm.quantity = 1; |
| | | requisitionForm.remark = ''; |
| | | selectedSparePartMaxQty.value = 1; |
| | | await loadRequisitionOptions(); |
| | | requisitionDialogVisible.value = true; |
| | | }; |
| | | |
| | | const onRequisitionSparePartChange = (val) => { |
| | | const selected = requisitionSparePartsOptions.value.find(item => item.id === val); |
| | | selectedSparePartMaxQty.value = selected ? selected.quantity : 1; |
| | | if (requisitionForm.quantity > selectedSparePartMaxQty.value) { |
| | | requisitionForm.quantity = selectedSparePartMaxQty.value; |
| | | } |
| | | }; |
| | | |
| | | const submitRequisition = async () => { |
| | | if (!requisitionFormRef.value) return; |
| | | try { |
| | | await requisitionFormRef.value.validate(); |
| | | } catch { |
| | | return; |
| | | } |
| | | if (requisitionForm.quantity <= 0) { |
| | | ElMessage.warning('领用数量必须大于0'); |
| | | return; |
| | | } |
| | | const selected = requisitionSparePartsOptions.value.find(item => item.id === requisitionForm.sparePartsId); |
| | | if (selected && requisitionForm.quantity > selected.quantity) { |
| | | ElMessage.warning(`库存不足,当前库存为 ${selected.quantity}`); |
| | | return; |
| | | } |
| | | requisitionLoading.value = true; |
| | | try { |
| | | const res = await outboundSpareParts({ |
| | | deviceLedgerId: requisitionForm.deviceLedgerId || undefined, |
| | | sparePartsId: requisitionForm.sparePartsId, |
| | | quantity: requisitionForm.quantity, |
| | | remark: requisitionForm.remark || undefined, |
| | | }); |
| | | if (res.code === 200) { |
| | | ElMessage.success('领用成功'); |
| | | requisitionDialogVisible.value = false; |
| | | fetchUsageData(); |
| | | } else { |
| | | ElMessage.error(res.msg || '领用失败'); |
| | | } |
| | | } catch { |
| | | ElMessage.error('领用失败'); |
| | | } finally { |
| | | requisitionLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // 组件挂载时获取列表数据 |
| | | onMounted(() => { |
| | | fetchListData(); |
| | |
| | | </el-button> |
| | | <el-button type="danger" |
| | | icon="Delete" |
| | | :disabled="multipleList.length <= 0 || hasFinishedStatus" |
| | | :disabled="multipleList.length <= 0" |
| | | @click="delRepairByIds(multipleList.map((item) => item.id))"> |
| | | 批量删除 |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | <PIMTable rowKey="id" |
| | | isSelection |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | | total: pagination.total, |
| | | }" |
| | | <el-table |
| | | :data="groupedDataList" |
| | | border |
| | | v-loading="tableLoading" |
| | | @selection-change="handleSelectionChange" |
| | | @pagination="changePage"> |
| | | <template #maintenanceResultRef="{ row }"> |
| | | <div>{{ row.maintenanceResult || '-' }}</div> |
| | | </template> |
| | | <template #statusRef="{ row }"> |
| | | <el-tag v-if="row.status === 2" |
| | | type="danger">失败</el-tag> |
| | | <el-tag v-if="row.status === 1" |
| | | type="success">完结</el-tag> |
| | | <el-tag v-if="row.status === 0" |
| | | type="warning">待保养</el-tag> |
| | | </template> |
| | | <template #operation="{ row }"> |
| | | <!-- 这个功能跟新增保养功能一模一样,有啥意义? --> |
| | | <!-- <el-button |
| | | type="primary" |
| | | text |
| | | @click="addMaintain(row)" |
| | | :expand-row-keys="expandedRowKeys" |
| | | :row-key="(row) => row.date" |
| | | @expand-change="onExpandChange" |
| | | > |
| | | 新增保养 |
| | | </el-button> --> |
| | | <el-button type="primary" |
| | | link |
| | | :disabled="row.status === 1" |
| | | @click="editPlan(row.id)"> |
| | | 编辑 |
| | | </el-button> |
| | | <el-button type="success" |
| | | link |
| | | :disabled="row.status === 1" |
| | | @click="addMaintain(row)"> |
| | | 保养 |
| | | </el-button> |
| | | <el-button type="danger" |
| | | link |
| | | :disabled="row.status === 1" |
| | | @click="delRepairByIds(row.id)"> |
| | | 删除 |
| | | </el-button> |
| | | <el-button type="primary" |
| | | link |
| | | @click="openFileDialog(row)"> |
| | | 附件 |
| | | <el-table-column type="selection" width="55" /> |
| | | <el-table-column type="expand"> |
| | | <template #default="props"> |
| | | <el-table :data="props.row.children" border row-key="id"> |
| | | <el-table-column label="设备名称" prop="deviceName" align="center" /> |
| | | <el-table-column label="规格型号" prop="deviceModel" align="center" /> |
| | | <el-table-column label="录入人" prop="createUserName" align="center" /> |
| | | <el-table-column label="保养项目" prop="machineryCategory" align="center"> |
| | | <template #default="{ row }"> |
| | | {{ row.machineryCategory || '--' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="实际保养人" prop="maintenanceActuallyName" align="center" /> |
| | | <el-table-column label="实际保养日期" align="center"> |
| | | <template #default="{ row }"> |
| | | {{ row.maintenanceActuallyTime ? dayjs(row.maintenanceActuallyTime).format('YYYY-MM-DD HH:mm:ss') : '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="保养结果" align="center"> |
| | | <template #default="{ row }"> |
| | | {{ row.maintenanceResult || '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="状态" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-tag v-if="row.status === 2" type="danger">失败</el-tag> |
| | | <el-tag v-if="row.status === 1" type="success">完结</el-tag> |
| | | <el-tag v-if="row.status === 0" type="warning">待保养</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" align="center" width="300"> |
| | | <template #default="{ row }"> |
| | | <el-button type="primary" link :disabled="row.status === 1" @click="editPlan(row.id)">编辑</el-button> |
| | | <el-button type="success" link :disabled="row.status !== 0" @click="handleSingleComplete(row.id)">保养完成</el-button> |
| | | <el-button type="success" link :disabled="row.status === 1" @click="addMaintain(row)">保养</el-button> |
| | | <el-button type="danger" link :disabled="row.status === 1" @click="delRepairByIds(row.id)">删除</el-button> |
| | | <el-button type="primary" link @click="openFileDialog(row)">附件</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="计划保养日期" prop="date" align="center" /> |
| | | <el-table-column label="保养记录数" prop="count" align="center" /> |
| | | <el-table-column label="待保养数" align="center" width="120"> |
| | | <template #default="{ row }"> |
| | | {{ row.children.filter(c => c.status === 0).length }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" align="center" width="200"> |
| | | <template #default="{ row }"> |
| | | <el-button |
| | | type="primary" |
| | | size="small" |
| | | :disabled="!row.children.some(c => c.status === 0)" |
| | | @click="handleBatchComplete(row.date)" |
| | | > |
| | | 一键保养完成 |
| | | </el-button> |
| | | </template> |
| | | </PIMTable> |
| | | </el-table-column> |
| | | </el-table> |
| | | <Pagination |
| | | v-show="pagination.total > 0" |
| | | :total="pagination.total" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :page="pagination.currentPage" |
| | | :limit="pagination.pageSize" |
| | | @pagination="changePage" |
| | | /> |
| | | </div> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | |
| | | v-model:visible="fileDialogVisible" |
| | | :record-type="'device_maintenance'" |
| | | :record-id="currentMaintenanceTaskId" /> |
| | | |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | delUpkeep, |
| | | deviceMaintenanceTaskList, |
| | | deviceMaintenanceTaskDel, |
| | | batchCompleteMaintenance, |
| | | completeMaintenance, |
| | | } from "@/api/equipmentManagement/upkeep"; |
| | | import dayjs from "dayjs"; |
| | | import Pagination from "@/components/PIMTable/Pagination.vue"; |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | const FileList = defineAsyncComponent(() => |
| | |
| | | const dataList = ref([]); |
| | | const pagination = ref({ |
| | | currentPage: 1, |
| | | pageSize: 10, |
| | | pageSize: 100, |
| | | total: 0, |
| | | }); |
| | | const multipleList = ref([]); |
| | | const tableLoading = ref(false); |
| | | const expandedRowKeys = ref([]); |
| | | |
| | | const groupByDate = (list) => { |
| | | const map = {}; |
| | | list.forEach(item => { |
| | | const date = item.dateStr || (item.maintenancePlanTime ? dayjs(item.maintenancePlanTime).format('YYYY-MM-DD') : (item.createTime ? dayjs(item.createTime).format('YYYY-MM-DD') : '未知日期')); |
| | | if (!map[date]) map[date] = []; |
| | | map[date].push(item); |
| | | }); |
| | | return Object.entries(map).map(([date, children]) => ({ date, children, count: children.length })); |
| | | }; |
| | | |
| | | const groupedDataList = computed(() => groupByDate(dataList.value)); |
| | | |
| | | // 单条保养完成 |
| | | const handleSingleComplete = async (id) => { |
| | | try { |
| | | await ElMessageBox.confirm('确认将该保养记录标记为完成?', '提示', { type: 'warning' }); |
| | | const res = await completeMaintenance(id); |
| | | if (res.code === 200) { |
| | | ElMessage.success('保养完成'); |
| | | getTableData(); |
| | | } else { |
| | | ElMessage.error(res.msg || '操作失败'); |
| | | } |
| | | } catch { /* 取消 */ } |
| | | }; |
| | | |
| | | // 一键保养完成 |
| | | const handleBatchComplete = async (dateStr) => { |
| | | try { |
| | | await ElMessageBox.confirm(`确认将 ${dateStr} 的所有待保养记录标记为完成?`, '提示', { type: 'warning' }); |
| | | const res = await batchCompleteMaintenance(dateStr); |
| | | if (res.code === 200) { |
| | | ElMessage.success(res.msg || '批量保养完成成功'); |
| | | getTableData(); |
| | | } else { |
| | | ElMessage.error(res.msg || '批量保养完成失败'); |
| | | } |
| | | } catch { /* 取消 */ } |
| | | }; |
| | | |
| | | const onExpandChange = (row, expandedRows) => { |
| | | expandedRowKeys.value = expandedRows.map(r => r.date); |
| | | }; |
| | | |
| | | // 保养任务tab相关变量 |
| | | const scheduledFilters = reactive({ |
| | |
| | | |
| | | // 保养记录相关方法(原设备保养页面方法) |
| | | const getTableData = async () => { |
| | | tableLoading.value = true; |
| | | try { |
| | | const params = { |
| | | current: pagination.value.currentPage, |
| | |
| | | } |
| | | } catch (error) { |
| | | console.log(error); |
| | | } finally { |
| | | tableLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | |
| | | }; |
| | | |
| | | const handleSelectionChange = selection => { |
| | | multipleList.value = selection; |
| | | }; |
| | | |
| | | // 检查选中的记录中是否有完结状态的 |
| | | const hasFinishedStatus = computed(() => { |
| | | return multipleList.value.some(item => item.status === 1); |
| | | // 从选中的分组行中收集所有子记录ID |
| | | const ids = []; |
| | | selection.forEach(group => { |
| | | if (group.children) { |
| | | group.children.forEach(child => ids.push(child.id)); |
| | | } |
| | | }); |
| | | multipleList.value = ids; |
| | | }; |
| | | |
| | | const changePage = page => { |
| | | pagination.value.currentPage = page.page; |
| | |
| | | }; |
| | | |
| | | const delRepairByIds = async ids => { |
| | | // 检查是否有完结状态的记录 |
| | | const hasFinished = multipleList.value.some(item => item.status === 1); |
| | | if (hasFinished) { |
| | | ElMessage.warning("不能删除状态为完结的记录"); |
| | | return; |
| | | } |
| | | const idList = Array.isArray(ids) ? ids : [ids]; |
| | | if (idList.length === 0) return; |
| | | |
| | | try { |
| | | await ElMessageBox.confirm("确认删除保养数据, 此操作不可逆?", "警告", { |
| | |
| | | type: "warning", |
| | | }); |
| | | |
| | | const { code } = await delUpkeep(ids); |
| | | const { code } = await delUpkeep(idList); |
| | | if (code === 200) { |
| | | ElMessage.success("删除成功"); |
| | | getTableData(); |
| | |
| | | }, |
| | | } |
| | | |
| | | const dateRange = inject('psiDateRange', null) |
| | | |
| | | const fetchData = () => { |
| | | productInOutAnalysis({ type: productType.value }) |
| | | const params = { type: productType.value, ...(dateRange?.value || {}) } |
| | | productInOutAnalysis(params) |
| | | .then((res) => { |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | const list = res.data |
| | |
| | | import Echarts from '@/components/Echarts/echarts.vue' |
| | | import { productTurnoverDays } from '@/api/viewIndex.js' |
| | | |
| | | const dateRange = inject('psiDateRange', null) |
| | | |
| | | const chartStyle = { width: '100%', height: '100%' } |
| | | const grid = { left: '3%', right: '4%', bottom: '3%', top: '4%', containLabel: true } |
| | | const barLegend = { show: false, textStyle: { color: '#B8C8E0' }, data: ['周转天数'] } |
| | |
| | | const yAxis1 = [{ type: 'value', axisLabel: { color: '#B8C8E0' } }] |
| | | |
| | | const fetchData = () => { |
| | | productTurnoverDays() |
| | | productTurnoverDays(dateRange?.value || undefined) |
| | | .then((res) => { |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | const list = res.data |
| | |
| | | |
| | | const compareClass = (val) => (val >= 0 ? 'compare-up' : 'compare-down') |
| | | |
| | | const dateRange = inject('psiDateRange', null) |
| | | |
| | | const fetchData = () => { |
| | | salesPurchaseStorageProductCount() |
| | | const params = dateRange?.value || {} |
| | | salesPurchaseStorageProductCount(params.startDate, params.endDate) |
| | | .then((res) => { |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | statItems.value = res.data.map((item) => ({ |
| | | name: item.name, |
| | | name: item.name === '销售产品数' ? '销售订单数' : item.name, |
| | | value: item.value, |
| | | rate: item.rate, |
| | | })) |
| | |
| | | import { rawMaterialPurchaseAmountRatio } from '@/api/viewIndex.js' |
| | | import { useChartBackground } from '@/hooks/useChartBackground.js' |
| | | |
| | | const dateRange = inject('psiDateRange', null) |
| | | |
| | | const pieWrapperRef = ref(null) |
| | | const pieBackgroundRef = ref(null) |
| | | |
| | |
| | | }) |
| | | |
| | | const fetchData = () => { |
| | | rawMaterialPurchaseAmountRatio() |
| | | rawMaterialPurchaseAmountRatio(dateRange?.value || undefined) |
| | | .then((res) => { |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | const items = res.data |
| | |
| | | import { ref, onMounted, onBeforeUnmount, computed, inject, watch } from 'vue' |
| | | import { productSalesAnalysis } from '@/api/viewIndex.js' |
| | | import PanelHeader from './PanelHeader.vue' |
| | | |
| | | const dateRange = inject('psiDateRange', null) |
| | | import CarouselCards from './CarouselCards.vue' |
| | | import Echarts from '@/components/Echarts/echarts.vue' |
| | | import { useChartBackground } from '@/hooks/useChartBackground.js' |
| | |
| | | }) |
| | | |
| | | const fetchData = () => { |
| | | productSalesAnalysis() |
| | | productSalesAnalysis(dateRange?.value || undefined) |
| | | .then((res) => { |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | const items = res.data |
| | |
| | | |
| | | <!-- 顶部标题栏 --> |
| | | <div class="dashboard-header"> |
| | | <div class="header-left"></div> |
| | | <div class="factory-name">PSI 数据分析</div> |
| | | <div class="header-right"> |
| | | <div class="date-picker-wrapper"> |
| | | <el-date-picker |
| | | v-model="psiDateRange" |
| | | type="daterange" |
| | | range-separator="至" |
| | | start-placeholder="开始日期" |
| | | end-placeholder="结束日期" |
| | | value-format="YYYY-MM-DD" |
| | | :shortcuts="dateShortcuts" |
| | | @change="onDateRangeChange" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 主要内容区域 --> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, onBeforeUnmount, nextTick, provide } from 'vue' |
| | | import { ref, onMounted, onBeforeUnmount, nextTick, provide, reactive } from 'vue' |
| | | import autofit from 'autofit.js' |
| | | import dayjs from 'dayjs' |
| | | import LeftBottom from './components/left-bottom.vue' |
| | | import CenterCenter from './components/center-center.vue' |
| | | import RightTop from '../dataDashboard/components/basic/right-top.vue' |
| | |
| | | |
| | | // 用户store |
| | | const userStore = useUserStore() |
| | | |
| | | // 日期范围筛选 |
| | | const psiDateRange = ref([]) |
| | | const psiDateRangeParams = ref({}) |
| | | const dateShortcuts = [ |
| | | { text: '本月', value: () => { const start = dayjs().startOf('month').format('YYYY-MM-DD'); const end = dayjs().format('YYYY-MM-DD'); return [start, end] } }, |
| | | { text: '近7天', value: () => { const start = dayjs().subtract(6, 'day').format('YYYY-MM-DD'); const end = dayjs().format('YYYY-MM-DD'); return [start, end] } }, |
| | | { text: '近30天', value: () => { const start = dayjs().subtract(29, 'day').format('YYYY-MM-DD'); const end = dayjs().format('YYYY-MM-DD'); return [start, end] } }, |
| | | { text: '近90天', value: () => { const start = dayjs().subtract(89, 'day').format('YYYY-MM-DD'); const end = dayjs().format('YYYY-MM-DD'); return [start, end] } }, |
| | | ] |
| | | |
| | | const onDateRangeChange = (val) => { |
| | | if (val && val.length === 2) { |
| | | psiDateRangeParams.value = { startDate: val[0], endDate: val[1] } |
| | | } else { |
| | | psiDateRangeParams.value = {} |
| | | } |
| | | dataDashboardRefreshTick.value++ |
| | | } |
| | | |
| | | provide('psiDateRange', psiDateRangeParams) |
| | | |
| | | /** 与 dataDashboard 共用注入名,子组件(含复用的 right-top/right-bottom)每分钟刷新 */ |
| | | const DASHBOARD_REFRESH_MS = 60 * 1000 |
| | |
| | | background-repeat: no-repeat; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .header-left { |
| | | width: 300px; |
| | | padding-left: 20px; |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .header-right { |
| | | width: 300px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: flex-end; |
| | | padding-right: 20px; |
| | | } |
| | | |
| | | .date-picker-wrapper { |
| | | /* 日期选择器在大屏上的样式 */ |
| | | --el-fill-color-blank: rgba(0, 20, 60, 0.8); |
| | | --el-border-color: rgba(0, 212, 255, 0.3); |
| | | --el-text-color-regular: #00d4ff; |
| | | --el-color-primary: #00d4ff; |
| | | } |
| | | |
| | | .factory-name { |
| | | font-weight: 600; |
| | | font-size: 52px; |
| | | color: #FFFFFF; |
| | | top: 16px; |
| | | position: absolute; |
| | | } |
| | | |
| | | .fullscreen-btn { |
| | |
| | | import DateTypeSwitch from '../DateTypeSwitch.vue' |
| | | import { customerContributionRanking } from '@/api/viewIndex.js' |
| | | |
| | | const dateRange = inject('psiDateRange', null) |
| | | |
| | | const chartStyle = { |
| | | width: '100%', |
| | | height: '100%', |
| | |
| | | } |
| | | |
| | | const fetchCustomerRanking = () => { |
| | | customerContributionRanking({ type: dateType.value }) |
| | | customerContributionRanking({ type: dateType.value, ...(dateRange?.value || {}) }) |
| | | .then((res) => { |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | dataArr.value = res.data.map(item => ({ |
| | |
| | | import DateTypeSwitch from '../DateTypeSwitch.vue' |
| | | import { supplierPurchaseRanking } from '@/api/viewIndex.js' |
| | | |
| | | const dateRange = inject('psiDateRange', null) |
| | | |
| | | const chartStyle = { |
| | | width: '100%', |
| | | height: '100%', |
| | |
| | | |
| | | // 供应商采购排名 |
| | | const fetchSupplierRanking = () => { |
| | | supplierPurchaseRanking({ type: radio1.value }) |
| | | supplierPurchaseRanking({ type: radio1.value, ...(dateRange?.value || {}) }) |
| | | .then((res) => { |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | dataArr.value = res.data.map(item => ({ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-form :model="filters" :inline="true"> |
| | | <el-form-item label="日期"> |
| | | <el-date-picker |
| | | style="width: 240px" |
| | | v-model="filters.month" |
| | | value-format="YYYY-MM" |
| | | format="YYYY-MM" |
| | | type="month" |
| | | placeholder="选择月份" |
| | | clearable |
| | | @change="getTableData" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="getTableData"> 搜索 </el-button> |
| | | <el-button @click="resetFilters"> 重置 </el-button> |
| | | <el-button @click="handleOut"> 导出 </el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | | total: pagination.total, |
| | | }" |
| | | @pagination="changePage" |
| | | /> |
| | | <div class="app-container vat-page"> |
| | | <el-row :gutter="20" class="vat-row"> |
| | | <!-- 左侧:增值税明细列表 --> |
| | | <el-col :span="15" class="vat-col"> |
| | | <el-card shadow="never" class="vat-card"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span class="card-title">增值税明细</span> |
| | | <el-select v-model="queryMonth" placeholder="选择月份" clearable style="width: 150px;" @change="handleMonthChange"> |
| | | <el-option v-for="m in monthOptions" :key="m" :label="m" :value="m" /> |
| | | </el-select> |
| | | </div> |
| | | </template> |
| | | <template v-if="!loading && vatDetailList.length === 0"> |
| | | <div class="empty-full"> |
| | | <el-empty description="暂无增值税明细数据" :image-size="160" /> |
| | | </div> |
| | | </template> |
| | | <template v-else> |
| | | <div class="table-wrapper"> |
| | | <el-table :data="vatDetailList" border v-loading="loading" stripe height="100%"> |
| | | <el-table-column prop="orderType" label="类型" width="65" align="center"> |
| | | <template #default="scope"> |
| | | <el-tag :type="scope.row.orderType === '进项' ? '' : 'warning'" size="small" effect="plain"> |
| | | {{ scope.row.orderType }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="invoiceNo" label="发票号" min-width="140" show-overflow-tooltip /> |
| | | <el-table-column prop="salesContractNo" label="合同号" min-width="140" show-overflow-tooltip /> |
| | | <el-table-column prop="supplierName" label="供应商" min-width="100" show-overflow-tooltip /> |
| | | <el-table-column prop="customerName" label="客户" min-width="100" show-overflow-tooltip /> |
| | | <el-table-column prop="invoiceDate" label="开票日期" width="110" align="center" /> |
| | | <el-table-column prop="taxRate" label="税率" width="75" align="center"> |
| | | <template #default="scope"> |
| | | {{ scope.row.taxRate }}% |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="taxAmount" label="税额" width="110" align="right"> |
| | | <template #default="scope"> |
| | | <span class="tax-amount" :class="scope.row.orderType === '进项' ? 'input-tax' : 'output-tax'"> |
| | | ¥{{ formatNumber(scope.row.taxAmount) }} |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | <el-pagination |
| | | v-if="page.total > 0" |
| | | class="vat-pagination" |
| | | background |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :current-page="page.current" |
| | | :page-sizes="[10, 20, 50]" |
| | | :page-size="page.size" |
| | | :total="page.total" |
| | | @size-change="handleSizeChange" |
| | | @current-change="handleCurrentChange" /> |
| | | </template> |
| | | </el-card> |
| | | </el-col> |
| | | |
| | | <!-- 右侧:柱状对比图 --> |
| | | <el-col :span="9" class="vat-col"> |
| | | <el-card shadow="never" class="vat-card"> |
| | | <template #header> |
| | | <span class="card-title">进销项增值税对比</span> |
| | | </template> |
| | | <div class="summary-row"> |
| | | <div class="summary-item input-bg"> |
| | | <div class="summary-label">进项税额合计</div> |
| | | <div class="summary-value">¥{{ formatNumber(inputTotal) }}</div> |
| | | </div> |
| | | <div class="summary-item output-bg"> |
| | | <div class="summary-label">销项税额合计</div> |
| | | <div class="summary-value">¥{{ formatNumber(outputTotal) }}</div> |
| | | </div> |
| | | <div class="summary-item diff-bg"> |
| | | <div class="summary-label">差额</div> |
| | | <div class="summary-value" :class="diffValue >= 0 ? 'positive' : 'negative'"> |
| | | {{ diffValue >= 0 ? '+' : '' }}¥{{ formatNumber(diffValue) }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div v-if="vatDetailList.length > 0" ref="vatChart" class="chart-container"></div> |
| | | <div v-else class="chart-empty">暂无数据</div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { usePaginationApi } from "@/hooks/usePaginationApi"; |
| | | import { onMounted, getCurrentInstance } from "vue"; |
| | | import { getTaxList } from "@/api/procurementManagement/taxComparison"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | import { ref, reactive, computed, onMounted, nextTick, onBeforeUnmount } from "vue"; |
| | | import * as echarts from 'echarts'; |
| | | import { getVatDetail } from "@/api/procurementManagement/taxComparison"; |
| | | |
| | | defineOptions({ |
| | | name: "增值税比对", |
| | | }); |
| | | |
| | | const { |
| | | loading, |
| | | filters, |
| | | columns, |
| | | dataList, |
| | | pagination, |
| | | getTableData, |
| | | resetFilters, |
| | | onCurrentChange, |
| | | } = usePaginationApi( |
| | | getTaxList, |
| | | { |
| | | month: [], // 来票日期 |
| | | }, |
| | | [ |
| | | { |
| | | label: "月份", |
| | | prop: "month", |
| | | align: "center", |
| | | }, |
| | | { |
| | | label: "销项税额", |
| | | prop: "jtaxAmount", |
| | | align: "center", |
| | | }, |
| | | { |
| | | label: "进项税额", |
| | | prop: "xtaxAmount", |
| | | align: "center", |
| | | }, |
| | | { |
| | | label: "销-进", |
| | | prop: "taxAmount", |
| | | align: "center", |
| | | }, |
| | | ], |
| | | {} |
| | | ); |
| | | const vatChart = ref(null); |
| | | let chartInstance = null; |
| | | |
| | | const changePage = ({ page }) => { |
| | | pagination.currentPage = page; |
| | | onCurrentChange(page); |
| | | const queryMonth = ref(new Date().toISOString().slice(0, 7)); |
| | | const monthOptions = ref([]); |
| | | const vatDetailList = ref([]); |
| | | const loading = ref(false); |
| | | const page = reactive({ current: 1, size: 20, total: 0 }); |
| | | |
| | | // 汇总额 |
| | | const inputTotal = computed(() => { |
| | | return vatDetailList.value |
| | | .filter(i => i.orderType === '进项') |
| | | .reduce((sum, i) => sum + (Number(i.taxAmount) || 0), 0); |
| | | }); |
| | | const outputTotal = computed(() => { |
| | | return vatDetailList.value |
| | | .filter(i => i.orderType === '销项') |
| | | .reduce((sum, i) => sum + (Number(i.taxAmount) || 0), 0); |
| | | }); |
| | | const diffValue = computed(() => outputTotal.value - inputTotal.value); |
| | | |
| | | const formatNumber = (val) => { |
| | | const num = Number(val); |
| | | if (isNaN(num)) return '0.00'; |
| | | return num.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); |
| | | }; |
| | | |
| | | // 导出 |
| | | const handleOut = () => { |
| | | ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "取消", |
| | | type: "warning", |
| | | const loadVatDetail = () => { |
| | | loading.value = true; |
| | | const params = { |
| | | current: page.current, |
| | | size: page.size, |
| | | month: queryMonth.value || undefined, |
| | | }; |
| | | getVatDetail(params) |
| | | .then((res) => { |
| | | if (res.code === 200) { |
| | | vatDetailList.value = res.data.records || []; |
| | | page.total = res.data.total || 0; |
| | | nextTick(() => renderChart()); |
| | | } |
| | | }) |
| | | .then(() => { |
| | | proxy.download("/purchase/report/exportTwo", {}, "增值税比对.xlsx"); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已取消"); |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | const renderChart = () => { |
| | | if (!vatChart.value) return; |
| | | if (chartInstance) { |
| | | chartInstance.dispose(); |
| | | } |
| | | chartInstance = echarts.init(vatChart.value); |
| | | |
| | | const data = vatDetailList.value; |
| | | const invoices = data.map(i => i.invoiceNo); |
| | | |
| | | chartInstance.setOption({ |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | backgroundColor: 'rgba(255,255,255,0.95)', |
| | | borderColor: '#e0e0e0', |
| | | borderWidth: 1, |
| | | textStyle: { color: '#333', fontSize: 13 }, |
| | | formatter: function (params) { |
| | | let html = `<b>${params[0].axisValue}</b><br/>`; |
| | | params.forEach(p => { |
| | | if (p.value > 0) { |
| | | html += `${p.marker} ${p.seriesName}: ¥${formatNumber(p.value)}<br/>`; |
| | | } |
| | | }); |
| | | return html; |
| | | }, |
| | | }, |
| | | legend: { |
| | | data: ['进项税额', '销项税额'], |
| | | bottom: 0, |
| | | textStyle: { fontSize: 12 }, |
| | | }, |
| | | grid: { |
| | | left: '10%', |
| | | right: '8%', |
| | | top: '8%', |
| | | bottom: '12%', |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: invoices, |
| | | axisLabel: { |
| | | rotate: 45, |
| | | fontSize: 10, |
| | | interval: 0, |
| | | }, |
| | | axisTick: { alignWithLabel: true }, |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | name: '税额(元)', |
| | | nameTextStyle: { fontSize: 11 }, |
| | | axisLabel: { |
| | | formatter: (val) => val >= 10000 ? `${(val / 10000).toFixed(1)}万` : val, |
| | | }, |
| | | }, |
| | | series: [ |
| | | { |
| | | name: '进项税额', |
| | | type: 'bar', |
| | | barWidth: '35%', |
| | | data: data.map(i => i.orderType === '进项' ? Number(i.taxAmount) || 0 : null), |
| | | itemStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: '#667eea' }, |
| | | { offset: 1, color: '#764ba2' }, |
| | | ]), |
| | | borderRadius: [4, 4, 0, 0], |
| | | }, |
| | | emphasis: { |
| | | itemStyle: { color: '#667eea' }, |
| | | }, |
| | | }, |
| | | { |
| | | name: '销项税额', |
| | | type: 'bar', |
| | | barWidth: '35%', |
| | | data: data.map(i => i.orderType === '销项' ? Number(i.taxAmount) || 0 : null), |
| | | itemStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: '#f093fb' }, |
| | | { offset: 1, color: '#f5576c' }, |
| | | ]), |
| | | borderRadius: [4, 4, 0, 0], |
| | | }, |
| | | emphasis: { |
| | | itemStyle: { color: '#f093fb' }, |
| | | }, |
| | | }, |
| | | ], |
| | | }); |
| | | }; |
| | | |
| | | const handleMonthChange = () => { |
| | | page.current = 1; |
| | | loadVatDetail(); |
| | | }; |
| | | |
| | | const handleSizeChange = (val) => { |
| | | page.size = val; |
| | | loadVatDetail(); |
| | | }; |
| | | |
| | | const handleCurrentChange = (val) => { |
| | | page.current = val; |
| | | loadVatDetail(); |
| | | }; |
| | | |
| | | const generateMonthOptions = () => { |
| | | const now = new Date(); |
| | | const options = []; |
| | | for (let i = 11; i >= 0; i--) { |
| | | const d = new Date(now.getFullYear(), now.getMonth() - i, 1); |
| | | const y = d.getFullYear(); |
| | | const m = String(d.getMonth() + 1).padStart(2, '0'); |
| | | options.push(`${y}-${m}`); |
| | | } |
| | | monthOptions.value = options; |
| | | }; |
| | | |
| | | const handleResize = () => { |
| | | if (chartInstance) { |
| | | chartInstance.resize(); |
| | | } |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getTableData(); |
| | | generateMonthOptions(); |
| | | loadVatDetail(); |
| | | window.addEventListener('resize', handleResize); |
| | | }); |
| | | |
| | | onBeforeUnmount(() => { |
| | | window.removeEventListener('resize', handleResize); |
| | | if (chartInstance) { |
| | | chartInstance.dispose(); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .table_list { |
| | | margin-top: unset; |
| | | .vat-page { |
| | | height: calc(100vh - 84px); |
| | | } |
| | | |
| | | .vat-row { |
| | | height: 100%; |
| | | } |
| | | |
| | | .vat-col { |
| | | height: 100%; |
| | | } |
| | | |
| | | .vat-card { |
| | | height: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | |
| | | :deep(.el-card__header) { |
| | | flex-shrink: 0; |
| | | } |
| | | :deep(.el-card__body) { |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | overflow: hidden; |
| | | } |
| | | } |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .card-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .table-wrapper { |
| | | flex: 1; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .vat-pagination { |
| | | flex-shrink: 0; |
| | | margin-top: 12px; |
| | | justify-content: flex-end; |
| | | } |
| | | |
| | | .tax-amount { |
| | | font-weight: 600; |
| | | font-family: 'Monaco', 'Menlo', monospace; |
| | | &.input-tax { color: #667eea; } |
| | | &.output-tax { color: #f5576c; } |
| | | } |
| | | |
| | | .summary-row { |
| | | display: flex; |
| | | gap: 12px; |
| | | margin-bottom: 16px; |
| | | flex-shrink: 0; |
| | | |
| | | .summary-item { |
| | | flex: 1; |
| | | padding: 12px 14px; |
| | | border-radius: 8px; |
| | | text-align: center; |
| | | |
| | | &.input-bg { |
| | | background: linear-gradient(135deg, #f3f0ff 0%, #e8e5ff 100%); |
| | | .summary-value { color: #667eea; } |
| | | } |
| | | &.output-bg { |
| | | background: linear-gradient(135deg, #fff0f3 0%, #ffe0e6 100%); |
| | | .summary-value { color: #f5576c; } |
| | | } |
| | | &.diff-bg { |
| | | background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); |
| | | .summary-value { |
| | | &.positive { color: #f5576c; } |
| | | &.negative { color: #22c55e; } |
| | | } |
| | | } |
| | | |
| | | .summary-label { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-bottom: 4px; |
| | | } |
| | | .summary-value { |
| | | font-size: 18px; |
| | | font-weight: 700; |
| | | font-family: 'Monaco', 'Menlo', monospace; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .chart-container { |
| | | flex: 1; |
| | | min-height: 0; |
| | | } |
| | | |
| | | .empty-full { |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .chart-empty { |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | color: #c0c4cc; |
| | | font-size: 13px; |
| | | } |
| | | </style> |