zhaoen 4 kuukautta sitten
vanhempi
sitoutus
7ffa4d4198

+ 512 - 0
src/views/dataManagement/threeModelManagement/index.vue

@@ -0,0 +1,512 @@
+<template>
+  <Container :query-form="queryForm" class="three-model-container">
+    <!-- 查询表单 -->
+    <template #query-form>
+      <el-form-item label="模型名称">
+        <el-input
+          v-model="queryForm.keyword"
+          placeholder="输入模型名称/编号"
+          clearable
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="模型类型">
+        <el-select v-model="queryForm.modelType" placeholder="全部" clearable>
+          <el-option label="建筑" value="building" />
+          <el-option label="装备" value="equipment" />
+          <el-option label="载具" value="vehicle" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="发布状态">
+        <el-select v-model="queryForm.publishStatus" placeholder="全部" clearable>
+          <el-option label="草稿" value="draft" />
+          <el-option label="已发布" value="published" />
+        </el-select>
+      </el-form-item>
+    </template>
+
+    <!-- 顶部操作 -->
+    <template #header-actions>
+      <el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
+      <el-button @click="resetQuery">重置</el-button>
+      <el-button type="primary" icon="el-icon-plus" @click="openAdd">上传模型</el-button>
+      <el-button type="danger" icon="el-icon-delete" :disabled="!selectedRows.length" @click="handleBatchDelete">
+        批量删除
+      </el-button>
+    </template>
+
+    <!-- 表格 -->
+    <div class="table-container">
+      <el-table
+        ref="table"
+        :data="tableData"
+        border
+        stripe
+        height="100%"
+        size="mini"
+        highlight-current-row
+        @selection-change="handleSelectionChange"
+      >
+        <el-table-column type="selection" width="55" />
+        <el-table-column type="index" label="序号" width="60" />
+        <el-table-column prop="code" label="模型编号" width="120" align="center" />
+        <el-table-column prop="name" label="模型名称" min-width="180" show-overflow-tooltip />
+        <el-table-column prop="typeLabel" label="模型类型" width="100" align="center" />
+        <el-table-column label="发布状态" width="100" align="center">
+          <template slot-scope="{row}">
+            <el-tag :type="row.publishStatus==='published' ? 'success' : 'info'" size="mini">
+              {{ publishMap[row.publishStatus] }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="version" label="版本" width="100" align="center" />
+        <el-table-column prop="secretLevel" label="密级" width="80" align="center" />
+        <el-table-column prop="sizeLabel" label="文件大小" width="100" align="center" />
+        <el-table-column prop="uploader" label="上传人" width="100" align="center" />
+        <el-table-column prop="createTime" label="创建时间" min-width="180" align="center" sortable />
+        <el-table-column label="操作" fixed="right" align="center" min-width="240">
+          <template slot-scope="{ row }">
+            <div class="action-bar">
+              <el-link type="primary" :underline="false" @click="handleView(row)">预览</el-link>
+              <el-link type="primary" :underline="false" @click="handleEdit(row)">编辑</el-link>
+              <el-link type="success" :underline="false" @click="togglePublish(row)">
+                {{ row.publishStatus==='published' ? '下线' : '发布' }}
+              </el-link>
+              <el-link type="danger" :underline="false" @click="handleDelete(row)">删除</el-link>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <div class="pagination-container">
+        <el-pagination
+          background
+          :current-page="pagination.currentPage"
+          :page-sizes="[10, 20, 50]"
+          :page-size="pagination.pageSize"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="pagination.total"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </div>
+
+    <!-- 底部操作 -->
+    <template #footer-actions>
+      <el-button icon="el-icon-download" @click="exportList">导出清单</el-button>
+      <span class="footer-tips">选中 {{ selectedRows.length }} 项</span>
+    </template>
+
+    <!-- 预览弹窗 -->
+    <el-dialog title="模型预览" :visible.sync="previewVisible" width="800px" class="model-dialog">
+      <el-image :src="currentRow.previewUrl || previewPlaceholder" fit="contain" style="width:100%;height:420px;" />
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="previewVisible=false">关闭</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 上传模型弹窗 -->
+    <el-dialog
+      title="上传三维模型"
+      :visible.sync="addVisible"
+      width="720px"
+      :close-on-click-modal="false"
+      class="model-dialog"
+      @closed="onAddClosed"
+    >
+      <el-form ref="addFormRef" :model="addForm" :rules="addRules" label-width="110px" size="small">
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="模型名称" prop="modelName">
+              <el-input v-model.trim="addForm.modelName" maxlength="200" show-word-limit placeholder="如:建筑模型-101" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="模型类型" prop="modelType">
+              <el-select v-model="addForm.modelType" placeholder="请选择">
+                <el-option label="建筑" value="building" />
+                <el-option label="装备" value="equipment" />
+                <el-option label="载具" value="vehicle" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="12">
+            <el-form-item label="模型分类" prop="modelCategory">
+              <el-select v-model="addForm.modelCategory" placeholder="请选择">
+                <el-option label="ZB模型" value="ZB" />
+                <el-option label="场景模型" value="SCENE" />
+                <el-option label="环境模型" value="ENV" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="12">
+            <el-form-item label="模型格式" prop="modelFormat">
+              <el-select v-model="addForm.modelFormat" placeholder="自动识别或手选" filterable allow-create default-first-option>
+                <el-option label="glb" value="glb" />
+                <el-option label="gltf" value="gltf" />
+                <el-option label="fbx" value="fbx" />
+                <el-option label="obj" value="obj" />
+                <el-option label="dae" value="dae" />
+                <el-option label="stl" value="stl" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="24">
+            <el-form-item label="模型文件" prop="modelFilePath">
+              <el-upload
+                ref="uploader"
+                :auto-upload="false"
+                :limit="1"
+                :show-file-list="true"
+                :on-change="onFileChange"
+                :on-remove="onFileRemove"
+                :before-upload="() => false"
+              >
+                <el-button icon="el-icon-upload2" type="primary" size="small">选择文件</el-button>
+                <span style="margin-left:10px;color:#94a3b8">支持 glb/gltf/fbx/obj/dae/stl(演示态不实际上传)</span>
+              </el-upload>
+              <div v-if="addForm.modelSize" style="margin-top:6px;color:#9fb3c8">
+                文件大小:{{ formatSize(addForm.modelSize) }},路径:{{ addForm.modelFilePath }}
+              </div>
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="24">
+            <el-form-item label="模型描述" prop="description">
+              <el-input
+                v-model.trim="addForm.description"
+                type="textarea"
+                :rows="3"
+                maxlength="500"
+                show-word-limit
+                placeholder="简要描述模型用途、来源或版本信息"
+              />
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="24">
+            <el-form-item label="模型标签" prop="tags">
+              <el-select
+                v-model="addForm.tags"
+                multiple
+                filterable
+                allow-create
+                default-first-option
+                placeholder="输入并回车可创建新标签"
+                style="width:100%"
+              >
+                <el-option v-for="t in tagOptions" :key="t" :label="t" :value="t" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="addVisible=false">取 消</el-button>
+        <el-button type="primary" @click="handleAddSubmit">确 定</el-button>
+      </div>
+    </el-dialog>
+  </Container>
+</template>
+
+<script>
+export default {
+  name: 'ThreeModelManagement',
+  data() {
+    return {
+      // 查询
+      queryForm: { keyword: '', modelType: '', publishStatus: '' },
+
+      // 列表&分页
+      tableData: [],
+      allData: [],
+      selectedRows: [],
+      pagination: { currentPage: 1, pageSize: 10, total: 0 },
+
+      // 预览
+      previewVisible: false,
+      currentRow: {},
+      publishMap: { draft: '草稿', published: '已发布' },
+      previewPlaceholder: 'https://dummyimage.com/800x420/0b254a/9db2c9&text=3D+Preview',
+
+      // 新增弹窗
+      addVisible: false,
+      addForm: {
+        modelName: '',
+        modelType: '',
+        modelCategory: '',
+        modelFilePath: '',
+        modelSize: 0,
+        modelFormat: '',
+        description: '',
+        tags: []
+      },
+      addRules: {
+        modelName: [{ required: true, message: '请输入模型名称', trigger: 'blur' }],
+        modelType: [{ required: true, message: '请选择模型类型', trigger: 'change' }],
+        modelCategory: [{ required: true, message: '请选择模型分类', trigger: 'change' }],
+        modelFilePath: [{ required: true, message: '请选择模型文件', trigger: 'change' }],
+        modelFormat: [{ required: true, message: '请选择或确认格式', trigger: 'change' }],
+        description: [{ min: 0, max: 500, message: '不超过500字', trigger: 'blur' }],
+        tags: [{ type: 'array', required: true, message: '至少添加1个标签', trigger: 'change' }]
+      },
+      tagOptions: ['建筑', '场景', '环境', '机密', '标准件', '高精度'],
+    }
+  },
+  created() {
+    this.initMock()
+    this.handleQuery()
+  },
+  methods: {
+    /* ====== 数据Mock ====== */
+    initMock() {
+      const types = { building: '建筑', equipment: '装备', vehicle: '载具' }
+      const states = ['draft', 'published']
+      const list = []
+      for (let i = 1; i <= 50; i++) {
+        const tKeys = Object.keys(types)
+        const type = tKeys[i % tKeys.length]
+        list.push({
+          code: `M-${1000 + i}`,
+          name: `${types[type]}模型-${i}`,
+          type,
+          typeLabel: types[type],
+          publishStatus: states[i % 2],
+          version: `V2.${i % 5 + 1}.0`,
+          secretLevel: ['秘密', '机密', '绝密'][i % 3],
+          sizeLabel: `${(Math.random() * 150 + 20).toFixed(1)} MB`,
+          uploader: ['张三', '李四', '系统'][i % 3],
+          createTime: `2023-12-${String(i % 28 + 1).padStart(2, '0')} 13:30:20`,
+          previewUrl: '',
+        })
+      }
+      this.allData = list
+    },
+
+    /* ====== 查询&分页 ====== */
+    handleQuery() {
+      let list = this.allData.slice()
+      const { keyword, modelType, publishStatus } = this.queryForm
+      if (keyword) list = list.filter(r => r.name.includes(keyword) || r.code.includes(keyword))
+      if (modelType) list = list.filter(r => r.type === modelType)
+      if (publishStatus) list = list.filter(r => r.publishStatus === publishStatus)
+      this.pagination.total = list.length
+      const start = (this.pagination.currentPage - 1) * this.pagination.pageSize
+      this.tableData = list.slice(start, start + this.pagination.pageSize)
+    },
+    resetQuery() {
+      this.queryForm = { keyword: '', modelType: '', publishStatus: '' }
+      this.pagination.currentPage = 1
+      this.handleQuery()
+    },
+    handleSizeChange(size) {
+      this.pagination.pageSize = size
+      this.pagination.currentPage = 1
+      this.handleQuery()
+    },
+    handleCurrentChange(p) {
+      this.pagination.currentPage = p
+      this.handleQuery()
+    },
+
+    /* ====== 表格操作 ====== */
+    handleSelectionChange(val) {
+      this.selectedRows = val
+    },
+    handleView(row) {
+      this.currentRow = row
+      this.previewVisible = true
+    },
+    handleEdit(row) {
+      this.$message.info(`编辑 ${row.name}`)
+    },
+    togglePublish(row) {
+      row.publishStatus = row.publishStatus === 'published' ? 'draft' : 'published'
+      this.$message.success('状态已更新')
+    },
+    handleDelete(row) {
+      this.allData = this.allData.filter(r => r.code !== row.code)
+      this.$message.success('删除成功')
+      this.handleQuery()
+    },
+    handleBatchDelete() {
+      const ids = new Set(this.selectedRows.map(r => r.code))
+      this.allData = this.allData.filter(r => !ids.has(r.code))
+      this.$refs.table && this.$refs.table.clearSelection()
+      this.selectedRows = []
+      this.$message.success('批量删除成功')
+      this.handleQuery()
+    },
+    exportList() {
+      this.$message.success('导出清单成功(占位)')
+    },
+
+    /* ====== 新增弹窗 ====== */
+    openAdd() {
+      this.addForm = {
+        modelName: '',
+        modelType: this.queryForm.modelType || '',
+        modelCategory: '',
+        modelFilePath: '',
+        modelSize: 0,
+        modelFormat: '',
+        description: '',
+        tags: []
+      }
+      this.addVisible = true
+      this.$nextTick(() => {
+        this.$refs.addFormRef && this.$refs.addFormRef.clearValidate()
+        this.$refs.uploader && this.$refs.uploader.clearFiles()
+      })
+    },
+    onAddClosed() {
+      this.$refs.addFormRef && this.$refs.addFormRef.resetFields()
+    },
+    onFileChange(file) {
+      if (!file) return
+      const name = file.name || ''
+      const size = file.size || 0
+      const fmt = this.inferFormat(name)
+      this.addForm.modelFilePath = name
+      this.addForm.modelSize = size
+      if (!this.addForm.modelFormat && fmt) this.addForm.modelFormat = fmt
+      this.$refs.addFormRef && this.$refs.addFormRef.validateField('modelFilePath')
+      this.$refs.addFormRef && this.$refs.addFormRef.validateField('modelFormat')
+    },
+    onFileRemove() {
+      this.addForm.modelFilePath = ''
+      this.addForm.modelSize = 0
+    },
+    inferFormat(filename) {
+      const m = (filename || '').toLowerCase().match(/\.([a-z0-9]+)$/)
+      return m ? m[1] : ''
+    },
+    formatSize(bytes) {
+      if (!bytes || bytes <= 0) return '0 B'
+      const units = ['B','KB','MB','GB','TB']
+      const i = Math.floor(Math.log(bytes) / Math.log(1024))
+      const val = bytes / Math.pow(1024, i)
+      return `${val.toFixed(2)} ${units[i]}`
+    },
+    handleAddSubmit() {
+      this.$refs.addFormRef.validate(valid => {
+        if (!valid) return
+        const typeMap = { building: '建筑', equipment: '装备', vehicle: '载具' }
+        const nextCode = this.genNextCode()
+        const record = {
+          code: nextCode,
+          name: this.addForm.modelName,
+          type: this.addForm.modelType,
+          typeLabel: typeMap[this.addForm.modelType] || this.addForm.modelType,
+          publishStatus: 'draft',
+          version: 'V1.0.0',
+          secretLevel: '秘密',
+          sizeLabel: this.addForm.modelSize ? this.formatSize(this.addForm.modelSize) : '--',
+          uploader: '系统',
+          createTime: this.nowStr(),
+          previewUrl: '',
+          _raw: { ...this.addForm } // 保留原始字段,便于后端打通
+        }
+        this.allData.unshift(record)
+        this.$message.success('上传成功(已加入草稿)')
+        this.pagination.currentPage = 1
+        this.addVisible = false
+        this.handleQuery()
+      })
+    },
+    genNextCode() {
+      const maxNum = this.allData.reduce((m, r) => {
+        const n = Number((r.code || '').replace(/^\D+/, '')) || 0
+        return Math.max(m, n)
+      }, 1000)
+      return `M-${maxNum + 1}`
+    },
+    nowStr() {
+      const d = new Date()
+      const pad = n => String(n).padStart(2, '0')
+      return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
+    },
+  },
+}
+</script>
+
+<style>
+/* 统一表格暗色风格,消除白色条纹和容器 */
+.three-model-container .el-table,
+.three-model-container .el-table__fixed,
+.three-model-container .el-table__fixed-right {
+  background: rgba(15, 31, 56, 0.86) !important;
+}
+.three-model-container .el-table__body-wrapper,
+.three-model-container .el-table__fixed-body-wrapper {
+  background: rgba(15, 31, 56, 0.86) !important;
+}
+.three-model-container .el-table td,
+.three-model-container .el-table th.is-leaf {
+  background: rgba(15, 31, 56, 0.86) !important;
+  border-color: rgba(255, 255, 255, 0.08) !important;
+  color: #e6efff;
+}
+.three-model-container .el-table th {
+  background: rgba(30, 55, 95, 0.85) !important;
+  color: #cfd9e6 !important;
+}
+.three-model-container .el-table--striped .el-table__body tr.el-table__row--striped td {
+  background: rgba(15, 31, 56, 0.9) !important;
+}
+.three-model-container .el-table__body tr:hover > td {
+  background: rgba(30, 64, 175, 0.25) !important;
+}
+.three-model-container .el-table__current-row > td {
+  background: rgba(64, 150, 255, 0.2) !important;
+}
+.three-model-container .el-table__fixed::before,
+.three-model-container .el-table__fixed-right::before,
+.three-model-container .el-table::before {
+  background-color: rgba(255, 255, 255, 0.08) !important;
+}
+.three-model-container .el-table__gutter,
+.three-model-container .el-table__fixed-right .el-table__fixed-header-wrapper,
+.three-model-container .el-table__fixed-right .el-table__fixed-body-wrapper,
+.three-model-container .el-table__fixed .el-table__fixed-header-wrapper,
+.three-model-container .el-table__fixed .el-table__fixed-body-wrapper {
+  background: rgba(15, 31, 56, 0.86) !important;
+}
+
+.pagination-container {
+  margin-top: 14px;
+  display: flex;
+  justify-content: flex-end;
+}
+.action-bar {
+  display: flex;
+  justify-content: space-around;
+  width: 100%;
+}
+.model-dialog .el-dialog {
+  background: #0b254a;
+  border: 1px solid #364a64;
+}
+.model-dialog .el-dialog__title {
+  color: #e6efff;
+}
+.model-dialog .el-dialog__header,
+.model-dialog .el-dialog__footer {
+  border-color: #364a64;
+}
+.model-dialog .el-upload-list__item {
+  background: rgba(255,255,255,0.04);
+  border-color: #364a64;
+  color: #e6efff;
+}
+.footer-tips {
+  color: #94a3b8;
+  margin-left: 10px;
+}
+</style>

+ 443 - 0
src/views/dataManagement/threeModelTypeManagement/index.vue

@@ -0,0 +1,443 @@
+<template>
+  <Container :query-form="queryForm" class="mtm-container">
+    <!-- 头部查询表单 -->
+    <template #query-form>
+      <el-form-item label="三维模型类型名称">
+        <el-input
+          v-model="queryForm.name"
+          placeholder="请输入三维模型类型名称"
+          clearable
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="三维模型类型码">
+        <el-select
+          v-model="queryForm.code"
+          placeholder="全部"
+          clearable
+          filterable
+          style="width: 180px"
+        >
+          <el-option label="ADMIN" value="ADMIN" />
+          <el-option label="EQUIP" value="EQUIP" />
+          <el-option label="ASSET" value="ASSET" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间">
+        <el-date-picker
+          v-model="queryForm.dateRange"
+          type="datetimerange"
+          range-separator="至"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+          value-format="yyyy-MM-dd HH:mm:ss"
+          clearable
+        />
+      </el-form-item>
+    </template>
+
+    <!-- 头部操作按钮 -->
+    <template #header-actions>
+      <el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
+      <el-button @click="resetQuery">重置</el-button>
+      <el-button type="primary" icon="el-icon-plus" @click="openAdd">新建</el-button>
+      <el-button
+        type="danger"
+        icon="el-icon-delete"
+        :disabled="!selectedRows.length"
+        @click="handleBatchDelete"
+      >批量删除</el-button>
+    </template>
+
+    <!-- 主内容区表格 -->
+    <div class="table-container">
+      <el-table
+        ref="table"
+        :data="tableData"
+        border
+        stripe
+        height="100%"
+        size="mini"
+        highlight-current-row
+        @selection-change="handleSelectionChange"
+      >
+        <el-table-column type="selection" width="55" />
+        <el-table-column type="index" label="序号" width="60" />
+        <el-table-column prop="id" label="编号" width="100" sortable align="center" />
+        <el-table-column
+          prop="name"
+          label="三维模型类型名称"
+          min-width="200"
+          align="center"
+          show-overflow-tooltip
+        />
+        <el-table-column
+          prop="code"
+          label="三维模型类型码"
+          min-width="160"
+          align="center"
+          :filters="[{text:'ADMIN',value:'ADMIN'},{text:'EQUIP',value:'EQUIP'},{text:'ASSET',value:'ASSET'}]"
+          :filter-method="(value, row) => row.code === value"
+          filter-placement="bottom-end"
+        />
+        <el-table-column prop="creator" label="创建人" width="120" align="center" />
+        <el-table-column prop="createTime" label="创建时间" min-width="180" align="center" sortable />
+        <el-table-column label="操作" fixed="right" align="center" min-width="220">
+          <template slot-scope="{ row }">
+            <div class="action-bar">
+              <el-link type="primary" :underline="false" @click="handleView(row)">查看</el-link>
+              <el-link type="primary" :underline="false" @click="handleEdit(row)">编辑</el-link>
+              <el-link type="danger"  :underline="false" @click="handleDelete(row)">删除</el-link>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <div class="pagination-container">
+        <el-pagination
+          background
+          :current-page="pagination.currentPage"
+          :page-sizes="[10, 20, 50, 100]"
+          :page-size="pagination.pageSize"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="pagination.total"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </div>
+
+    <!-- 底部操作 -->
+    <template #footer-actions>
+      <span class="footer-tips">共 {{ pagination.total }} 条记录</span>
+    </template>
+
+    <!-- 详情弹窗 -->
+    <el-dialog
+      title="类型详情"
+      :visible.sync="detailVisible"
+      width="520px"
+      :close-on-click-modal="false"
+      class="mtm-dialog"
+    >
+      <el-descriptions :column="1" border size="small">
+        <el-descriptions-item label="编号">{{ currentRow.id }}</el-descriptions-item>
+        <el-descriptions-item label="名称">{{ currentRow.name }}</el-descriptions-item>
+        <el-descriptions-item label="类型码">{{ currentRow.code }}</el-descriptions-item>
+        <el-descriptions-item label="创建人">{{ currentRow.creator }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ currentRow.createTime }}</el-descriptions-item>
+      </el-descriptions>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="detailVisible=false">关闭</el-button>
+        <el-button type="primary" @click="detailVisible=false">确定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 新建弹窗 -->
+    <el-dialog
+      title="新建三维模型类型"
+      :visible.sync="addVisible"
+      width="520px"
+      :close-on-click-modal="false"
+      class="mtm-dialog"
+      @closed="onAddClosed"
+    >
+      <el-form ref="addFormRef" :model="addForm" :rules="addRules" label-width="110px" size="small">
+        <el-form-item label="类型名称" prop="name">
+          <el-input v-model.trim="addForm.name" maxlength="100" show-word-limit placeholder="请输入类型名称" />
+        </el-form-item>
+        <el-form-item label="类型码" prop="code">
+          <el-select v-model="addForm.code" placeholder="请选择类型码" style="width: 220px">
+            <el-option label="ADMIN" value="ADMIN" />
+            <el-option label="EQUIP" value="EQUIP" />
+            <el-option label="ASSET" value="ASSET" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="创建人" prop="creator">
+          <el-input v-model.trim="addForm.creator" placeholder="例如:ADMIN/张三" />
+        </el-form-item>
+        <el-form-item label="创建时间" prop="createTime">
+          <el-date-picker
+            v-model="addForm.createTime"
+            type="datetime"
+            value-format="yyyy-MM-dd HH:mm:ss"
+            placeholder="选择时间"
+            style="width: 220px"
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="addVisible=false">取 消</el-button>
+        <el-button type="primary" @click="handleAddSubmit">确 定</el-button>
+      </div>
+    </el-dialog>
+  </Container>
+</template>
+
+<script>
+export default {
+  name: 'ModelTypeManagement',
+  data () {
+    return {
+      // 查询表单
+      queryForm: {
+        name: '',
+        code: '',
+        dateRange: []
+      },
+      // 表格
+      tableData: [],
+      allData: [],
+      selectedRows: [],
+
+      // 分页
+      pagination: {
+        currentPage: 51, // 跟原型一致
+        pageSize: 10,
+        total: 0
+      },
+
+      // 详情
+      detailVisible: false,
+      currentRow: {},
+
+      // 新建
+      addVisible: false,
+      addForm: {
+        name: '',
+        code: '',
+        creator: 'ADMIN',
+        createTime: ''
+      },
+      addRules: {
+        name: [
+          { required: true, message: '请输入类型名称', trigger: 'blur' },
+          { min: 1, max: 100, message: '长度在 1~100 个字符', trigger: 'blur' }
+        ],
+        code: [{ required: true, message: '请选择类型码', trigger: 'change' }],
+        creator: [{ required: true, message: '请输入创建人', trigger: 'blur' }],
+        createTime: [{ required: true, message: '请选择创建时间', trigger: 'change' }]
+      }
+    }
+  },
+  created () {
+    this.initMock()
+    this.handleQuery()
+  },
+  methods: {
+    /* ------------ 工具 ------------- */
+    nowStr () {
+      const d = new Date()
+      const pad = n => n.toString().padStart(2, '0')
+      return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
+    },
+
+    /* ------------ 数据 ------------- */
+    initMock () {
+      const creators = ['ADMIN', '系统', '张三', '李四']
+      const arr = []
+      for (let i = 1; i <= 658; i++) {
+        arr.push({
+          id: i,
+          name: `设备${(i % 9) + 1}`,
+          code: ['ADMIN', 'EQUIP', 'ASSET'][i % 3],
+          creator: creators[i % creators.length],
+          createTime: `2021-02-${(i % 28 + 1).toString().padStart(2,'0')} 10:30:00`
+        })
+      }
+      this.allData = arr
+    },
+
+    /* ------------ 查询&分页 ------------- */
+    handleQuery () {
+      // 过滤
+      let list = this.allData.slice()
+
+      if (this.queryForm.name) {
+        const kw = this.queryForm.name.trim()
+        list = list.filter(r => r.name.includes(kw))
+      }
+      if (this.queryForm.code) {
+        list = list.filter(r => r.code === this.queryForm.code)
+      }
+      if (this.queryForm.dateRange && this.queryForm.dateRange.length === 2) {
+        const [start, end] = this.queryForm.dateRange
+        const s = new Date(start).getTime()
+        const e = new Date(end).getTime()
+        list = list.filter(r => {
+          const t = new Date(r.createTime).getTime()
+          return t >= s && t <= e
+        })
+      }
+
+      this.pagination.total = list.length
+      // 分页
+      const startIdx = (this.pagination.currentPage - 1) * this.pagination.pageSize
+      this.tableData = list.slice(startIdx, startIdx + this.pagination.pageSize)
+    },
+    resetQuery () {
+      this.queryForm = { name: '', code: '', dateRange: [] }
+      this.pagination.currentPage = 1
+      this.handleQuery()
+    },
+    handleSizeChange (size) {
+      this.pagination.pageSize = size
+      this.pagination.currentPage = 1
+      this.handleQuery()
+    },
+    handleCurrentChange (page) {
+      this.pagination.currentPage = page
+      this.handleQuery()
+    },
+
+    /* ------------ 选择/操作 ------------- */
+    handleSelectionChange (val) {
+      this.selectedRows = val
+    },
+    handleView (row) {
+      this.currentRow = { ...row }
+      this.detailVisible = true
+    },
+    handleEdit (row) {
+      this.$message.info(`编辑:#${row.id} ${row.name}(占位)`)
+    },
+    handleDelete (row) {
+      this.$confirm(`确认删除「${row.name}」吗?`, '提示', { type: 'warning' })
+        .then(() => {
+          this.allData = this.allData.filter(r => r.id !== row.id)
+          this.$message.success('删除成功')
+          // 若删空当前页,回退一页
+          if ((this.pagination.currentPage - 1) * this.pagination.pageSize >= this.allData.length) {
+            this.pagination.currentPage = Math.max(1, Math.ceil(this.allData.length / this.pagination.pageSize))
+          }
+          this.handleQuery()
+        })
+        .catch(() => {})
+    },
+    handleBatchDelete () {
+      const ids = new Set(this.selectedRows.map(r => r.id))
+      if (!ids.size) return
+      this.$confirm(`确认删除选中的 ${ids.size} 条记录吗?`, '警告', { type: 'warning' })
+        .then(() => {
+          this.allData = this.allData.filter(r => !ids.has(r.id))
+          this.$refs.table && this.$refs.table.clearSelection()
+          this.selectedRows = []
+          this.$message.success('批量删除成功')
+          if ((this.pagination.currentPage - 1) * this.pagination.pageSize >= this.allData.length) {
+            this.pagination.currentPage = Math.max(1, Math.ceil(this.allData.length / this.pagination.pageSize))
+          }
+          this.handleQuery()
+        })
+        .catch(() => {})
+    },
+
+    /* ------------ 新建 ------------- */
+    openAdd () {
+      // 预填创建时间为此刻
+      this.addForm = {
+        name: '',
+        code: '',
+        creator: 'ADMIN',
+        createTime: this.nowStr()
+      }
+      this.addVisible = true
+      this.$nextTick(() => this.$refs.addFormRef && this.$refs.addFormRef.clearValidate())
+    },
+    handleAddSubmit () {
+      this.$refs.addFormRef.validate(valid => {
+        if (!valid) return
+        // 生成自增 id(mock 环境)
+        const maxId = this.allData.length ? Math.max.apply(null, this.allData.map(r => r.id)) : 0
+        const record = {
+          id: maxId + 1,
+          name: this.addForm.name.trim(),
+          code: this.addForm.code,
+          creator: this.addForm.creator.trim() || 'ADMIN',
+          createTime: this.addForm.createTime
+        }
+        // 插入到数据源(放到最前面更直观)
+        this.allData.unshift(record)
+        this.$message.success('新增成功')
+        // 回到第 1 页(或保留当前页也行,看产品习惯)
+        this.pagination.currentPage = 1
+        this.addVisible = false
+        this.handleQuery()
+      })
+    },
+    onAddClosed () {
+      // 关闭时重置表单,避免下次残留
+      this.$refs.addFormRef && this.$refs.addFormRef.resetFields()
+    }
+  }
+}
+</script>
+
+<!-- 暗色风格 -->
+<style>
+.mtm-container .el-table {
+  flex: 1;
+  color: var(--dark-text-primary);
+  border-color: var(--dark-border-color);
+}
+.mtm-container .el-table th {
+  background-color: rgba(30,55,95,.3);
+  border-color: var(--dark-border-color);
+  color: var(--dark-text-primary);
+}
+.mtm-container .el-table tr { background-color: var(--dark-card-bg); }
+.mtm-container .el-table tr:hover > td { background-color: rgba(45,75,120,.2); }
+.mtm-container .el-table td { border-color: var(--dark-border-color); }
+.mtm-container .el-table--striped .el-table__body tr.el-table__row--striped td {
+  background-color: rgba(30,55,95,.1);
+}
+.mtm-container .el-table__current-row td { background-color: rgba(64,150,255,.15) !important; }
+
+.mtm-container .el-pagination { color: var(--dark-text-secondary); }
+.mtm-container .el-pagination button,
+.mtm-container .el-pagination span:not([class*='el-icon']),
+.mtm-container .el-pagination .el-pager li {
+  color: var(--dark-text-secondary);
+  background-color: var(--dark-card-bg);
+  border-color: var(--dark-border-color);
+}
+.mtm-container .el-pagination .el-pager li.active {
+  background-color: var(--dark-primary-color); color:#fff;
+}
+.mtm-container .el-pagination .el-pager li:hover { color: var(--dark-primary-color); }
+
+.mtm-container .el-form-item__label { color: var(--dark-text-secondary); }
+.mtm-container .el-input__inner,
+.mtm-container .el-select__inner,
+.mtm-container .el-date-editor .el-input__inner {
+  background-color: var(--dark-bg-color);
+  border-color: var(--dark-border-color);
+  color: var(--dark-text-primary);
+}
+
+.mtm-dialog .el-dialog { background-color: var(--dark-card-bg); border:1px solid var(--dark-border-color); }
+.mtm-dialog .el-dialog__title { color: var(--dark-text-primary); }
+.mtm-dialog .el-dialog__header { border-bottom: 1px solid var(--dark-border-color); }
+.mtm-dialog .el-dialog__footer { border-top: 1px solid var(--dark-border-color); }
+</style>
+
+<style scoped>
+.table-container {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+.pagination-container {
+  margin-top: 14px;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+}
+.action-bar {
+  display: flex;
+  justify-content: space-around;
+  align-items: center;
+  width: 100%;
+}
+.footer-tips { color: var(--dark-text-secondary); }
+</style>