| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858 |
- <template>
- <div class="fixed-assets-table-container">
- <el-table
- :data="flattenedTableData"
- border
- style="width: 100%"
- size="small"
- :row-class-name="getRowClassName"
- >
- <!-- 序号列 -->
- <el-table-column prop="seq" label="序号" width="80" align="center">
- <template slot-scope="scope">
- <span v-if="scope.row.isCategory" class="category-seq">
- {{ scope.row.categorySeq }}
- </span>
- <span v-else>{{ scope.row.seq }}</span>
- </template>
- </el-table-column>
- <!-- 项目列 -->
- <el-table-column
- prop="itemName"
- label="项目"
- min-width="200"
- align="left"
- >
- <template slot-scope="scope">
- <span v-if="scope.row.isCategory" class="category-name">
- {{ scope.row.itemName }}
- </span>
- <el-input
- v-else
- v-model="scope.row.itemName"
- placeholder="请输入项目名称"
- size="mini"
- :disabled="isViewMode"
- />
- </template>
- </el-table-column>
- <!-- 计量单位列 -->
- <el-table-column prop="unit" label="计量单位" width="120" align="center">
- <template slot-scope="scope">
- <el-input
- v-if="!scope.row.isCategory"
- v-model="scope.row.unit"
- placeholder="单位"
- size="mini"
- :disabled="isViewMode"
- />
- </template>
- </el-table-column>
- <!-- 固定资产原值列 -->
- <el-table-column
- prop="originalValue"
- label="固定资产原值"
- width="150"
- align="center"
- >
- <template slot-scope="scope">
- <el-input
- v-if="!scope.row.isCategory"
- v-model="scope.row.originalValue"
- placeholder="原值"
- size="mini"
- :disabled="isViewMode"
- @blur="handleCellBlur(scope.row, 'originalValue')"
- />
- </template>
- </el-table-column>
- <!-- 入帐或竣工验收日期列 -->
- <el-table-column
- prop="entryDate"
- label="入帐或竣工验收日期"
- width="180"
- align="center"
- >
- <template slot-scope="scope">
- <el-date-picker
- v-if="!scope.row.isCategory"
- v-model="scope.row.entryDate"
- type="date"
- placeholder="选择日期"
- size="mini"
- format="yyyy-MM-dd"
- value-format="yyyy-MM-dd"
- :disabled="isViewMode"
- style="width: 100%"
- />
- </template>
- </el-table-column>
- <!-- 折旧年限列 -->
- <el-table-column
- prop="depreciationPeriod"
- label="折旧年限"
- width="120"
- align="center"
- >
- <template slot-scope="scope">
- <el-input
- v-if="!scope.row.isCategory"
- v-model="scope.row.depreciationPeriod"
- placeholder="年限"
- size="mini"
- :disabled="isViewMode"
- @blur="handleCellBlur(scope.row, 'depreciationPeriod')"
- />
- </template>
- </el-table-column>
- <!-- 折旧费列 -->
- <el-table-column
- prop="depreciationExpense"
- label="折旧费"
- width="120"
- align="center"
- >
- <template slot-scope="scope">
- <el-input
- v-if="!scope.row.isCategory"
- v-model="scope.row.depreciationExpense"
- placeholder="费用"
- size="mini"
- :disabled="isViewMode"
- @blur="handleCellBlur(scope.row, 'depreciationExpense')"
- />
- </template>
- </el-table-column>
- <!-- 资金来源列 -->
- <el-table-column
- prop="fundSource"
- label="资金来源"
- width="120"
- align="center"
- >
- <template slot-scope="scope">
- <el-input
- v-if="!scope.row.isCategory"
- v-model="scope.row.fundSource"
- placeholder="来源"
- size="mini"
- :disabled="isViewMode"
- />
- </template>
- </el-table-column>
- <!-- 备注列 -->
- <el-table-column prop="remark" label="备注" min-width="150">
- <template slot-scope="scope">
- <el-input
- v-if="!scope.row.isCategory"
- v-model="scope.row.remark"
- placeholder="备注"
- size="mini"
- :disabled="isViewMode"
- />
- </template>
- </el-table-column>
- <!-- 操作列 -->
- <el-table-column label="操作" width="100" align="center" fixed="right">
- <template slot-scope="scope">
- <div v-if="scope.row.isCategory" class="operation-buttons">
- <el-button
- type="text"
- size="mini"
- icon="el-icon-plus"
- :disabled="isViewMode"
- @click="handleAddRow(scope.row)"
- />
- <el-button
- type="text"
- size="mini"
- icon="el-icon-minus"
- :disabled="isViewMode"
- @click="handleDeleteRow(scope.row)"
- />
- </div>
- </template>
- </el-table-column>
- </el-table>
- </div>
- </template>
- <script>
- import { Message } from 'element-ui'
- export default {
- name: 'FixedAssetsTable',
- props: {
- // 表格数据配置(嵌套结构)
- tableItems: {
- type: Array,
- default: () => [],
- },
- // 是否有保存的数据
- savedData: {
- type: Object,
- default: () => ({}),
- },
- // 是否查看模式
- isViewMode: {
- type: Boolean,
- default: false,
- },
- },
- data() {
- return {
- // 嵌套的表格数据
- fixedAssetsData: [],
- // 验证错误
- validationErrors: [],
- // 扁平化的表格数据(响应式)
- flattenedData: [],
- }
- },
- computed: {
- // 扁平化的表格数据(从嵌套结构生成)
- flattenedTableData() {
- return this.flattenedData
- },
- },
- watch: {
- tableItems: {
- handler(newVal) {
- if (newVal && newVal.length > 0) {
- this.fixedAssetsData = this.deepClone(newVal)
- } else {
- this.fixedAssetsData = this.getDefaultTableData()
- }
- // 重新生成扁平数据
- this.generateFlattenedData()
- },
- immediate: true,
- deep: true,
- },
- savedData: {
- handler() {
- // 数据变化时重新生成扁平数据
- this.generateFlattenedData()
- },
- deep: true,
- },
- },
- methods: {
- // 生成扁平数据
- generateFlattenedData() {
- const result = []
- let seq = 1
- const processItem = (item, parentCategory = null) => {
- if (item.isCategory) {
- // 分类行
- result.push({
- ...item,
- seq: item.categorySeq || item.id,
- isCategory: true,
- categorySeq: item.categorySeq || item.id,
- })
- // 处理分类下的子项
- if (item.children && Array.isArray(item.children)) {
- item.children.forEach((child) => {
- processItem(child, item)
- })
- }
- } else {
- // 普通行
- const rowData = {
- ...item,
- seq: seq++,
- isCategory: false,
- }
- // 如果有父分类,设置分类信息
- if (parentCategory) {
- rowData.categoryId = parentCategory.id
- rowData.categorySeq =
- parentCategory.categorySeq || parentCategory.id
- }
- // 初始化字段
- if (rowData.itemName === undefined) rowData.itemName = ''
- if (rowData.unit === undefined) rowData.unit = ''
- if (rowData.originalValue === undefined) rowData.originalValue = ''
- if (rowData.entryDate === undefined) rowData.entryDate = ''
- if (rowData.depreciationPeriod === undefined)
- rowData.depreciationPeriod = ''
- if (rowData.depreciationExpense === undefined)
- rowData.depreciationExpense = ''
- if (rowData.fundSource === undefined) rowData.fundSource = ''
- if (rowData.remark === undefined) rowData.remark = ''
- // 如果有保存的数据,填充值
- if (this.savedData) {
- // 从保存的数据中查找对应的值
- const savedItem = this.findSavedItemById(item.id)
- if (savedItem) {
- Object.keys(savedItem).forEach((key) => {
- if (savedItem[key] !== undefined && key !== 'id') {
- rowData[key] = savedItem[key]
- }
- })
- }
- }
- result.push(rowData)
- // 如果有子项,递归处理
- if (item.children && Array.isArray(item.children)) {
- item.children.forEach((child) => {
- processItem(child, item)
- })
- }
- }
- }
- // 处理所有项
- this.fixedAssetsData.forEach((item) => {
- processItem(item)
- })
- // 使用 Vue.set 确保响应式
- this.$set(this, 'flattenedData', result)
- },
- // 深拷贝
- deepClone(obj) {
- if (obj === null || typeof obj !== 'object') return obj
- if (obj instanceof Date) return new Date(obj.getTime())
- if (obj instanceof Array) return obj.map((item) => this.deepClone(item))
- if (typeof obj === 'object') {
- const clonedObj = {}
- for (const key in obj) {
- if (obj.hasOwnProperty(key)) {
- clonedObj[key] = this.deepClone(obj[key])
- }
- }
- return clonedObj
- }
- },
- // 获取默认表格数据
- getDefaultTableData() {
- return [
- {
- id: 'I',
- itemName: '房屋建筑物',
- isCategory: true,
- categorySeq: 'I',
- children: [
- {
- id: 'I-1',
- itemName: '办公用房',
- unit: '',
- originalValue: '',
- entryDate: '',
- depreciationPeriod: '',
- depreciationExpense: '',
- fundSource: '',
- remark: '',
- },
- {
- id: 'I-2',
- itemName: '教保用房',
- unit: '',
- originalValue: '',
- entryDate: '',
- depreciationPeriod: '',
- depreciationExpense: '',
- fundSource: '',
- remark: '',
- },
- {
- id: 'I-3',
- itemName: '幼儿宿舍用房',
- unit: '',
- originalValue: '',
- entryDate: '',
- depreciationPeriod: '',
- depreciationExpense: '',
- fundSource: '',
- remark: '',
- },
- {
- id: 'I-4',
- itemName: '其它',
- unit: '',
- originalValue: '',
- entryDate: '',
- depreciationPeriod: '',
- depreciationExpense: '',
- fundSource: '',
- remark: '',
- },
- ],
- },
- {
- id: 'II',
- itemName: '交通运输工具',
- isCategory: true,
- categorySeq: 'II',
- children: [
- {
- id: 'II-1',
- itemName: '车辆',
- unit: '',
- originalValue: '',
- entryDate: '',
- depreciationPeriod: '',
- depreciationExpense: '',
- fundSource: '',
- remark: '',
- },
- ],
- },
- {
- id: 'III',
- itemName: '教保专用设备',
- isCategory: true,
- categorySeq: 'III',
- children: [
- {
- id: 'III-1',
- itemName: '电教',
- unit: '',
- originalValue: '',
- entryDate: '',
- depreciationPeriod: '',
- depreciationExpense: '',
- fundSource: '',
- remark: '',
- },
- {
- id: 'III-2',
- itemName: '文体',
- unit: '',
- originalValue: '',
- entryDate: '',
- depreciationPeriod: '',
- depreciationExpense: '',
- fundSource: '',
- remark: '',
- },
- ],
- },
- {
- id: 'IV',
- itemName: '办公设备',
- isCategory: true,
- categorySeq: 'IV',
- children: [
- {
- id: 'IV-1',
- itemName: '电脑',
- unit: '',
- originalValue: '',
- entryDate: '',
- depreciationPeriod: '',
- depreciationExpense: '',
- fundSource: '',
- remark: '',
- },
- ],
- },
- {
- id: 'V',
- itemName: '其它固定资产',
- isCategory: true,
- categorySeq: 'V',
- children: [
- {
- id: 'V-1',
- itemName: '空调',
- unit: '',
- originalValue: '',
- entryDate: '',
- depreciationPeriod: '',
- depreciationExpense: '',
- fundSource: '',
- remark: '',
- },
- {
- id: 'V-2',
- itemName: '家电',
- unit: '',
- originalValue: '',
- entryDate: '',
- depreciationPeriod: '',
- depreciationExpense: '',
- fundSource: '',
- remark: '',
- },
- {
- id: 'V-3',
- itemName: '供水系统',
- unit: '',
- originalValue: '',
- entryDate: '',
- depreciationPeriod: '',
- depreciationExpense: '',
- fundSource: '',
- remark: '',
- },
- {
- id: 'V-4',
- itemName: '洗涤用具',
- unit: '',
- originalValue: '',
- entryDate: '',
- depreciationPeriod: '',
- depreciationExpense: '',
- fundSource: '',
- remark: '',
- },
- {
- id: 'V-5',
- itemName: '家具',
- unit: '',
- originalValue: '',
- entryDate: '',
- depreciationPeriod: '',
- depreciationExpense: '',
- fundSource: '',
- remark: '',
- },
- {
- id: 'V-6',
- itemName: '炊事用具',
- unit: '',
- originalValue: '',
- entryDate: '',
- depreciationPeriod: '',
- depreciationExpense: '',
- fundSource: '',
- remark: '',
- },
- {
- id: 'V-7',
- itemName: '其它',
- unit: '',
- originalValue: '',
- entryDate: '',
- depreciationPeriod: '',
- depreciationExpense: '',
- fundSource: '',
- remark: '',
- },
- ],
- },
- ]
- },
- // 获取行样式类名
- getRowClassName({ row }) {
- if (row.isCategory) {
- return 'category-row'
- }
- return ''
- },
- // 添加行
- handleAddRow(row) {
- // 找到对应的分类
- const category = this.findCategoryById(row.id)
- if (category) {
- // 生成新的项目ID
- const newId = `${row.id}-${Date.now()}`
- const newItem = {
- id: newId,
- itemName: '',
- unit: '',
- originalValue: '',
- entryDate: '',
- depreciationPeriod: '',
- depreciationExpense: '',
- fundSource: '',
- remark: '',
- }
- // 在分类的 children 数组末尾添加新行
- if (!category.children) {
- this.$set(category, 'children', [])
- }
- category.children.push(newItem)
- // 重新生成扁平数据
- this.generateFlattenedData()
- Message.success('添加行成功')
- }
- },
- // 删除行(删除分类下的最后一个子项)
- handleDeleteRow(row) {
- // row 是分类行,需要找到该分类下的子项
- const category = this.findCategoryById(row.id)
- if (category && category.children && category.children.length > 0) {
- // 删除最后一个子项
- category.children.pop()
- // 重新生成扁平数据
- this.generateFlattenedData()
- Message.success('删除成功')
- } else {
- Message.warning('该分类下没有可删除的行')
- }
- },
- // 根据ID在保存的数据中查找
- findSavedItemById(id) {
- if (!this.savedData) return null
- // 递归查找
- const findInArray = (items) => {
- for (const item of items) {
- if (item.id === id) {
- return item
- }
- if (item.children && Array.isArray(item.children)) {
- const found = findInArray(item.children)
- if (found) return found
- }
- }
- return null
- }
- // 如果 savedData 是数组
- if (Array.isArray(this.savedData)) {
- return findInArray(this.savedData)
- }
- // 如果 savedData 是对象,尝试查找
- if (typeof this.savedData === 'object') {
- // 可能是一个映射对象,key 是 id
- if (this.savedData[id]) {
- return this.savedData[id]
- }
- // 或者是嵌套结构
- return findInArray([this.savedData])
- }
- return null
- },
- // 根据ID查找分类
- findCategoryById(id) {
- const findInArray = (items) => {
- for (const item of items) {
- if (item.id === id) {
- return item
- }
- if (item.children && Array.isArray(item.children)) {
- const found = findInArray(item.children)
- if (found) return found
- }
- }
- return null
- }
- return findInArray(this.fixedAssetsData)
- },
- // 单元格失焦验证
- handleCellBlur(row, field) {
- // 实时验证格式
- if (field === 'originalValue' || field === 'depreciationExpense') {
- const value = row[field]
- if (value && !/^\d+(\.\d+)?$/.test(value)) {
- Message.warning(
- `${this.getFieldLabel(field)}格式不正确,请输入数字`
- )
- }
- }
- if (field === 'depreciationPeriod') {
- const value = row[field]
- if (value && !/^\d+$/.test(value)) {
- Message.warning(`${this.getFieldLabel(field)}必须是正整数`)
- }
- }
- },
- // 获取字段标签
- getFieldLabel(field) {
- const labels = {
- originalValue: '固定资产原值',
- depreciationPeriod: '折旧年限',
- depreciationExpense: '折旧费',
- }
- return labels[field] || field
- },
- // 验证表单
- validate() {
- this.validationErrors = []
- const errors = []
- // 验证扁平数据(因为用户编辑的是扁平数据)
- const flatData = this.flattenedTableData
- flatData.forEach((item, index) => {
- if (!item.isCategory) {
- // 非空验证
- if (!item.itemName || String(item.itemName).trim() === '') {
- errors.push(`第${item.seq}行:项目名称不能为空`)
- }
- if (!item.unit || String(item.unit).trim() === '') {
- errors.push(`第${item.seq}行:计量单位不能为空`)
- }
- if (
- !item.originalValue ||
- String(item.originalValue).trim() === ''
- ) {
- errors.push(`第${item.seq}行:固定资产原值不能为空`)
- }
- if (!item.entryDate || String(item.entryDate).trim() === '') {
- errors.push(`第${item.seq}行:入帐或竣工验收日期不能为空`)
- }
- if (
- !item.depreciationPeriod ||
- String(item.depreciationPeriod).trim() === ''
- ) {
- errors.push(`第${item.seq}行:折旧年限不能为空`)
- }
- if (
- !item.depreciationExpense ||
- String(item.depreciationExpense).trim() === ''
- ) {
- errors.push(`第${item.seq}行:折旧费不能为空`)
- }
- if (!item.fundSource || String(item.fundSource).trim() === '') {
- errors.push(`第${item.seq}行:资金来源不能为空`)
- }
- // 格式验证
- if (
- item.originalValue &&
- String(item.originalValue).trim() !== '' &&
- !/^\d+(\.\d+)?$/.test(String(item.originalValue))
- ) {
- errors.push(`第${item.seq}行:固定资产原值格式不正确,请输入数字`)
- }
- if (
- item.depreciationPeriod &&
- String(item.depreciationPeriod).trim() !== '' &&
- !/^\d+$/.test(String(item.depreciationPeriod))
- ) {
- errors.push(`第${item.seq}行:折旧年限必须是正整数`)
- }
- if (
- item.depreciationExpense &&
- String(item.depreciationExpense).trim() !== '' &&
- !/^\d+(\.\d+)?$/.test(String(item.depreciationExpense))
- ) {
- errors.push(`第${item.seq}行:折旧费格式不正确,请输入数字`)
- }
- }
- })
- this.validationErrors = errors
- return errors.length === 0
- },
- // 获取表格数据(用于保存)
- // 需要将扁平数据同步回嵌套结构
- getTableData() {
- // 同步扁平数据的修改到嵌套结构
- const flatData = this.flattenedData
- const syncDataToNested = (items) => {
- return items.map((item) => {
- if (item.isCategory) {
- // 分类行
- const newItem = { ...item }
- if (item.children && Array.isArray(item.children)) {
- newItem.children = syncDataToNested(item.children)
- }
- return newItem
- } else {
- // 普通行,从扁平数据中同步
- const flatItem = flatData.find(
- (f) => f.id === item.id && !f.isCategory
- )
- if (flatItem) {
- return {
- ...item,
- itemName: flatItem.itemName,
- unit: flatItem.unit,
- originalValue: flatItem.originalValue,
- entryDate: flatItem.entryDate,
- depreciationPeriod: flatItem.depreciationPeriod,
- depreciationExpense: flatItem.depreciationExpense,
- fundSource: flatItem.fundSource,
- remark: flatItem.remark,
- }
- }
- return item
- }
- })
- }
- return syncDataToNested(this.fixedAssetsData)
- },
- },
- }
- </script>
- <style scoped lang="scss">
- .fixed-assets-table-container {
- // 分类行样式
- ::v-deep .category-row {
- background-color: #f5f7fa !important;
- td {
- background-color: #f5f7fa !important;
- font-weight: bold;
- }
- .category-name {
- color: #409eff;
- font-weight: bold;
- }
- .category-seq {
- color: #409eff;
- font-weight: bold;
- }
- }
- // 操作按钮样式
- .operation-buttons {
- display: flex;
- justify-content: center;
- gap: 5px;
- .el-button {
- padding: 5px;
- min-width: 24px;
- height: 24px;
- border-radius: 50%;
- background-color: #000;
- color: #fff;
- border: none;
- &:hover {
- background-color: #333;
- }
- i {
- font-size: 14px;
- }
- }
- }
- // 输入框样式
- ::v-deep .el-input__inner {
- border: 1px solid #dcdfe6;
- border-radius: 4px;
- &:focus {
- border-color: #409eff;
- }
- }
- // 日期选择器样式
- ::v-deep .el-date-editor {
- width: 100%;
- }
- }
- </style>
|