zss
2 天以前 d51e4238adb806e6f5f9c20f20efc215532142d3
Merge remote-tracking branch 'origin/dev_New' into dev_New
已添加6个文件
3296 ■■■■■ 文件已修改
src/views/equipmentManagement/attendanceManagement/index.vue 403 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/transportTaskManagement/index.vue 692 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/vehicleFuelManagement/index.vue 556 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/vehicleManagement/index.vue 581 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/attendanceCheckin/index.vue 469 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/afterSalesTraceability/index.vue 595 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/attendanceManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,403 @@
<template>
  <div class="app-container">
    <!-- æœåŠ¡è¯„ä»·æ¦‚è§ˆï¼šæ¨¡æ‹Ÿå‘˜å·¥ä¸šç»©è¯„åˆ† -->
    <el-row :gutter="16" class="mb16">
      <el-col :span="8">
        <el-card shadow="never">
          <div class="kpi-title">本月平均评分</div>
          <div class="kpi-value">
            {{ overallAvgScore.toFixed(1) }}
            <span class="kpi-unit">分</span>
          </div>
          <el-rate v-model="overallAvgScore" disabled show-score score-template="{value} / 5" />
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card shadow="never">
          <div class="kpi-title">已评价维修工单</div>
          <div class="kpi-value">
            {{ ratedCount }}
            <span class="kpi-unit">单</span>
          </div>
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card shadow="never">
          <div class="kpi-title">待评价维修工单</div>
          <div class="kpi-value kpi-warning">
            {{ pendingCount }}
            <span class="kpi-unit">单</span>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <!-- æŸ¥è¯¢æ¡ä»¶ï¼šç®¡ç†å‘˜æŒ‰å·¥ç¨‹å¸ˆ / å®¢æˆ· / æ—¶é—´è¿½æº¯è¯„ä»· -->
    <div class="search_form">
      <div>
        <span class="search_title">维修工程师:</span>
        <el-input
          v-model="searchForm.engineerName"
          placeholder="请输入工程师姓名"
          style="width: 180px"
          clearable
          @keyup.enter.native="handleQuery"
        />
        <span class="search_title ml10">客户名称:</span>
        <el-input
          v-model="searchForm.customerName"
          placeholder="请输入客户名称"
          style="width: 180px"
          clearable
          @keyup.enter.native="handleQuery"
        />
        <span class="search_title ml10">完成时间:</span>
        <el-date-picker
          v-model="searchForm.dateRange"
          type="daterange"
          range-separator="至"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          clearable
        />
        <span class="search_title ml10">评价状态:</span>
        <el-select
          v-model="searchForm.status"
          placeholder="请选择"
          style="width: 140px"
          clearable
        >
          <el-option label="待评价" value="pending" />
          <el-option label="已评价" value="rated" />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
        <el-button icon="Download" @click="handleExport">
          å¯¼å‡ºè¯„价统计
        </el-button>
      </div>
    </div>
    <!-- ç»´ä¿®è¯„价列表:模拟“维修完成后触发评价”场景 -->
    <div class="table_list">
      <el-table
        :data="tableData"
        border
        style="width: 100%"
        height="calc(100vh - 24em)"
        :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
      >
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column prop="orderNo" label="维修工单号" width="160" show-overflow-tooltip />
        <el-table-column prop="deviceName" label="设备名称" width="160" show-overflow-tooltip />
        <el-table-column prop="customerName" label="客户名称" width="180" show-overflow-tooltip />
        <el-table-column prop="engineerName" label="维修工程师" width="120" />
        <el-table-column prop="completeTime" label="维修完成时间" width="180" />
        <el-table-column prop="score" label="星级评分" width="140" align="center">
          <template #default="scope">
            <el-rate v-if="scope.row.score" v-model="scope.row.score" disabled />
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column prop="status" label="评价状态" width="100" align="center">
          <template #default="scope">
            <el-tag
              :type="scope.row.status === 'rated' ? 'success' : 'warning'"
              size="small"
            >
              {{ scope.row.status === 'rated' ? '已评价' : '待评价' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="feedback" label="客户反馈" show-overflow-tooltip />
        <el-table-column label="操作" width="160" align="center" fixed="right">
          <template #default="scope">
            <el-button
              v-if="scope.row.status === 'pending'"
              type="primary"
              link
              size="small"
              @click="openEvaluate(scope.row)"
            >
              åŽ»è¯„ä»·
            </el-button>
            <el-button
              v-else
              type="primary"
              link
              size="small"
              @click="openEvaluate(scope.row)"
            >
              æŸ¥çœ‹ / ä¿®æ”¹è¯„ä»·
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <!-- è¯„价弹框:模拟“维修完成后弹出客户端评价” -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="520px"
      destroy-on-close
    >
      <div class="dialog-order-info" v-if="currentOrder">
        <div>维修工单:{{ currentOrder.orderNo }}</div>
        <div>设备名称:{{ currentOrder.deviceName }}</div>
        <div>维修工程师:{{ currentOrder.engineerName }}</div>
      </div>
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="100px"
      >
        <el-form-item label="星级评分:" prop="score">
          <el-rate v-model="form.score" :max="5" />
        </el-form-item>
        <el-form-item label="文字反馈:" prop="feedback">
          <el-input
            v-model="form.feedback"
            type="textarea"
            :rows="4"
            placeholder="请填写对本次维修服务的评价,如响应速度、专业程度、沟通体验等"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="dialogVisible = false">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="handleSubmit">提 äº¤ è¯„ ä»·</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, computed } from "vue";
import { ElMessage } from "element-plus";
// æ¨¡æ‹Ÿç»´ä¿®å·¥å• + å®¢æˆ·è¯„价数据
const rawOrders = ref([
  {
    id: 1,
    orderNo: "WX-2024-1201-001",
    deviceName: "空压机 A1 å·",
    customerName: "华南电子科技有限公司",
    engineerName: "王师傅",
    completeTime: "2024-12-01 10:30:00",
    completeDate: "2024-12-01",
    status: "rated",
    score: 5,
    feedback: "维修非常专业,响应速度快,现场解释也很清晰,满意。",
  },
  {
    id: 2,
    orderNo: "WX-2024-1201-002",
    deviceName: "注塑机 B3 å·",
    customerName: "华东精密制造有限公司",
    engineerName: "李师傅",
    completeTime: "2024-12-01 15:20:00",
    completeDate: "2024-12-01",
    status: "rated",
    score: 4,
    feedback: "整体还不错,就是到场时间稍微长了一点,希望后面能再快一些。",
  },
  {
    id: 3,
    orderNo: "WX-2024-1202-003",
    deviceName: "焊接机器人 C2 å·",
    customerName: "西南新能源科技股份",
    engineerName: "张师傅",
    completeTime: "2024-12-02 11:05:00",
    completeDate: "2024-12-02",
    status: "pending",
    score: null,
    feedback: "",
  },
  {
    id: 4,
    orderNo: "WX-2024-1203-005",
    deviceName: "测试台 D1 å·",
    customerName: "北方汽车零部件有限公司",
    engineerName: "王师傅",
    completeTime: "2024-12-03 09:50:00",
    completeDate: "2024-12-03",
    status: "pending",
    score: null,
    feedback: "",
  },
]);
// æŸ¥è¯¢è¡¨å•
const searchForm = reactive({
  engineerName: "",
  customerName: "",
  dateRange: [],
  status: "",
});
// åˆ—表数据
const tableData = ref([...rawOrders.value]);
// ç»Ÿè®¡ï¼šæ•´ä½“评分、已评价 / å¾…评价数量
const ratedOrders = computed(() =>
  rawOrders.value.filter((o) => o.status === "rated" && o.score)
);
const overallAvgScore = computed(() => {
  if (!ratedOrders.value.length) return 0;
  const sum = ratedOrders.value.reduce((acc, cur) => acc + (cur.score || 0), 0);
  return sum / ratedOrders.value.length;
});
const ratedCount = computed(() => ratedOrders.value.length);
const pendingCount = computed(
  () => rawOrders.value.filter((o) => o.status === "pending").length
);
// æŸ¥è¯¢ / é‡ç½®
const recomputeTable = () => {
  const list = rawOrders.value.filter((item) => {
    if (
      searchForm.engineerName &&
      !item.engineerName.includes(searchForm.engineerName.trim())
    ) {
      return false;
    }
    if (
      searchForm.customerName &&
      !item.customerName.includes(searchForm.customerName.trim())
    ) {
      return false;
    }
    if (searchForm.status && item.status !== searchForm.status) {
      return false;
    }
    if (Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) {
      const [start, end] = searchForm.dateRange;
      if (item.completeDate < start || item.completeDate > end) {
        return false;
      }
    }
    return true;
  });
  tableData.value = list;
};
const handleQuery = () => {
  recomputeTable();
};
const resetSearch = () => {
  searchForm.engineerName = "";
  searchForm.customerName = "";
  searchForm.dateRange = [];
  searchForm.status = "";
  recomputeTable();
};
// å¯¼å‡ºï¼ˆæ¼”示)
const handleExport = () => {
  ElMessage.success("当前为演示页面,评价导出功能未对接实际接口");
};
// è¯„价弹框
const dialogVisible = ref(false);
const dialogTitle = ref("维修服务评价");
const currentOrder = ref(null);
const formRef = ref(null);
const form = reactive({
  score: 0,
  feedback: "",
});
const rules = {
  score: [{ required: true, message: "请选择星级评分", trigger: "change" }],
  feedback: [{ required: true, message: "请填写文字反馈", trigger: "blur" }],
};
// æ‰“开评价:模拟“维修完成确认后弹出评价弹框”
const openEvaluate = (row) => {
  currentOrder.value = row;
  dialogTitle.value =
    row.status === "pending" ? "维修服务评价" : "查看 / ä¿®æ”¹è¯„ä»·";
  form.score = row.score || 0;
  form.feedback = row.feedback || "";
  dialogVisible.value = true;
};
// æäº¤è¯„价:同步到本地“员工业绩统计”
const handleSubmit = () => {
  if (!formRef.value) return;
  formRef.value.validate((valid) => {
    if (!valid || !currentOrder.value) return;
    const target = rawOrders.value.find((o) => o.id === currentOrder.value.id);
    if (target) {
      target.score = form.score;
      target.feedback = form.feedback;
      target.status = "rated";
    }
    ElMessage.success("评价提交成功,已同步至员工业绩统计");
    dialogVisible.value = false;
    recomputeTable();
  });
};
// åˆå§‹åŒ–列表
recomputeTable();
</script>
<style scoped lang="scss">
.mb16 {
  margin-bottom: 16px;
}
.kpi-title {
  font-size: 13px;
  color: #909399;
}
.kpi-value {
  margin-top: 6px;
  font-size: 24px;
  font-weight: 600;
  color: #303133;
}
.kpi-unit {
  font-size: 12px;
  margin-left: 4px;
  color: #909399;
}
.kpi-warning {
  color: #e6a23c;
}
.dialog-order-info {
  margin-bottom: 12px;
  font-size: 13px;
  color: #606266;
  line-height: 1.8;
}
.dialog-footer {
  text-align: right;
}
</style>
src/views/inventoryManagement/transportTaskManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,692 @@
<template>
  <div class="app-container">
    <!-- ç»Ÿè®¡æ¦‚览 -->
    <el-row :gutter="16" style="margin-bottom: 16px">
      <el-col :span="6">
        <el-card shadow="never">
          <div>总任务数</div>
          <div style="font-size: 22px; font-weight: 600; margin-top: 4px">
            {{ totalTasks }}
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="never">
          <div>进行中任务</div>
          <div style="font-size: 22px; font-weight: 600; margin-top: 4px">
            {{ runningTasks }}
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="never">
          <div>已完成任务</div>
          <div style="font-size: 22px; font-weight: 600; margin-top: 4px">
            {{ finishedTasks }}
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="never">
          <div>完成率</div>
          <div style="font-size: 22px; font-weight: 600; margin-top: 4px">
            {{ completionRate }}%
          </div>
        </el-card>
      </el-col>
    </el-row>
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <div class="search_form">
      <div>
        <span class="search_title">任务编号:</span>
        <el-input
          v-model="searchForm.taskNo"
          style="width: 200px"
          placeholder="请输入任务编号"
          clearable
          @keyup.enter.native="handleQuery"
        />
        <span class="search_title ml10">车辆编号:</span>
        <el-input
          v-model="searchForm.vehicleCode"
          style="width: 200px"
          placeholder="请输入车辆编号"
          clearable
          @keyup.enter.native="handleQuery"
        />
        <span class="search_title ml10">任务日期:</span>
        <el-date-picker
          v-model="searchForm.dateRange"
          type="daterange"
          range-separator="至"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          clearable
          @change="handleQuery"
        />
        <span class="search_title ml10">状态:</span>
        <el-select
          v-model="searchForm.status"
          style="width: 140px"
          placeholder="请选择任务状态"
          clearable
        >
          <el-option
            v-for="item in statusOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
        <el-button type="primary" icon="Plus" @click="openAdd">
          æ–°å»ºè¿è¾“任务
        </el-button>
      </div>
    </div>
    <!-- è¡¨æ ¼ -->
    <div class="table_list">
      <el-table
        :data="tableData"
        border
        style="width: 100%"
        height="calc(100vh - 22em)"
        :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
        :row-class-name="tableRowClassName"
      >
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column
          prop="taskNo"
          label="任务编号"
          width="150"
          show-overflow-tooltip
        />
        <el-table-column
          prop="outboundOrderNo"
          label="出库订单号"
          width="180"
          show-overflow-tooltip
        />
        <el-table-column
          prop="vehicleCode"
          label="车辆编号"
          width="130"
          show-overflow-tooltip
        />
        <el-table-column
          prop="plateNumber"
          label="车牌号码"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          prop="driverName"
          label="司机"
          width="100"
          show-overflow-tooltip
        />
        <el-table-column
          prop="loadAddress"
          label="装货地点"
          width="160"
          show-overflow-tooltip
        />
        <el-table-column
          prop="deliveryAddress"
          label="送货地点"
          width="160"
          show-overflow-tooltip
        />
        <el-table-column
          prop="loadTime"
          label="装货时间"
          width="160"
          show-overflow-tooltip
        />
        <el-table-column
          prop="deliveryTime"
          label="送货时间"
          width="160"
          show-overflow-tooltip
        />
        <el-table-column
          prop="signTime"
          label="签收时间"
          width="160"
          show-overflow-tooltip
        />
        <el-table-column label="状态" width="110" align="center">
          <template #default="scope">
            <el-tag :type="statusTagType(scope.row.status)">
              {{ scope.row.status }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="进度" width="150" align="center">
          <template #default="scope">
            <el-progress
              :percentage="scope.row.progress"
              :status="scope.row.status === '已完成' ? 'success' : undefined"
              :stroke-width="12"
              :show-text="false"
            />
            <div style="font-size: 12px; margin-top: 4px">
              {{ scope.row.progress }}%
            </div>
          </template>
        </el-table-column>
        <el-table-column label="操作" fixed="right" width="160" align="center">
          <template #default="scope">
            <el-button
              type="primary"
              link
              size="small"
              @click="openEdit(scope.row)"
            >
              ç¼–辑
            </el-button>
            <el-button
              type="danger"
              link
              size="small"
              @click="removeRow(scope.row)"
            >
              åˆ é™¤
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <!-- æ–°å¢ž/编辑弹窗 -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="780px"
      destroy-on-close
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="110px"
        label-position="right"
      >
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="任务编号:" prop="taskNo">
              <el-input
                v-model="form.taskNo"
                placeholder="请输入任务编号"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="出库订单号:" prop="outboundOrderNo">
              <el-input
                v-model="form.outboundOrderNo"
                placeholder="请输入关联出库订单号"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="车辆编号:" prop="vehicleCode">
              <el-input
                v-model="form.vehicleCode"
                placeholder="请输入车辆编号"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="车牌号码:" prop="plateNumber">
              <el-input
                v-model="form.plateNumber"
                placeholder="请输入车牌号码"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="司机:" prop="driverName">
              <el-input
                v-model="form.driverName"
                placeholder="请输入司机姓名"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="司机电话:" prop="driverPhone">
              <el-input
                v-model="form.driverPhone"
                placeholder="请输入司机联系电话"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="装货地点:" prop="loadAddress">
              <el-input
                v-model="form.loadAddress"
                placeholder="请输入装货地点"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="送货地点:" prop="deliveryAddress">
              <el-input
                v-model="form.deliveryAddress"
                placeholder="请输入送货地点"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="8">
            <el-form-item label="装货时间:" prop="loadTime">
              <el-date-picker
                v-model="form.loadTime"
                type="datetime"
                value-format="YYYY-MM-DD HH:mm:ss"
                format="YYYY-MM-DD HH:mm"
                placeholder="请选择装货时间"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="送货时间:" prop="deliveryTime">
              <el-date-picker
                v-model="form.deliveryTime"
                type="datetime"
                value-format="YYYY-MM-DD HH:mm:ss"
                format="YYYY-MM-DD HH:mm"
                placeholder="请选择送货时间"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="签收时间:" prop="signTime">
              <el-date-picker
                v-model="form.signTime"
                type="datetime"
                value-format="YYYY-MM-DD HH:mm:ss"
                format="YYYY-MM-DD HH:mm"
                placeholder="请选择签收时间"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="状态:" prop="status">
              <el-select v-model="form.status" placeholder="请选择任务状态">
                <el-option
                  v-for="item in statusOptions"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="计划日期:" prop="planDate">
              <el-date-picker
                v-model="form.planDate"
                type="date"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                placeholder="请选择计划日期"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="handleCancel">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="handleSubmit">保 å­˜</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
// æ¨¡æ‹Ÿè¿è¾“任务数据
const rawTasks = ref([
  {
    id: 1,
    taskNo: "T2024-1201-001",
    outboundOrderNo: "OUT-2024-1201-1001",
    vehicleCode: "CL-202401",
    plateNumber: "粤A12345",
    driverName: "张师傅",
    driverPhone: "13800000001",
    loadAddress: "深圳仓库A区",
    deliveryAddress: "广州客户一部",
    planDate: "2024-12-01",
    loadTime: "2024-12-01 09:00:00",
    deliveryTime: "2024-12-01 14:30:00",
    signTime: "2024-12-01 15:00:00",
    status: "已完成",
  },
  {
    id: 2,
    taskNo: "T2024-1201-002",
    outboundOrderNo: "OUT-2024-1201-1002",
    vehicleCode: "CL-202402",
    plateNumber: "粤B67890",
    driverName: "李师傅",
    driverPhone: "13800000002",
    loadAddress: "深圳仓库B区",
    deliveryAddress: "东莞客户二部",
    planDate: "2024-12-01",
    loadTime: "2024-12-01 10:00:00",
    deliveryTime: "2024-12-01 13:00:00",
    signTime: "",
    status: "运输中",
  },
  {
    id: 3,
    taskNo: "T2024-1202-001",
    outboundOrderNo: "OUT-2024-1202-1003",
    vehicleCode: "CL-202401",
    plateNumber: "粤A12345",
    driverName: "张师傅",
    driverPhone: "13800000001",
    loadAddress: "深圳仓库A区",
    deliveryAddress: "佛山客户三部",
    planDate: "2024-12-02",
    loadTime: "2024-12-02 08:30:00",
    deliveryTime: "",
    signTime: "",
    status: "待发车",
  },
  {
    id: 4,
    taskNo: "T2024-1203-001",
    outboundOrderNo: "OUT-2024-1203-1004",
    vehicleCode: "CL-202403",
    plateNumber: "粤C11223",
    driverName: "王师傅",
    driverPhone: "13800000003",
    loadAddress: "深圳仓库C区",
    deliveryAddress: "惠州客户四部",
    planDate: "2024-12-03",
    loadTime: "",
    deliveryTime: "",
    signTime: "",
    status: "未开始",
  },
]);
// çŠ¶æ€æžšä¸¾
const statusOptions = [
  { label: "未开始", value: "未开始" },
  { label: "待发车", value: "待发车" },
  { label: "运输中", value: "运输中" },
  { label: "待签收", value: "待签收" },
  { label: "已完成", value: "已完成" },
];
// æŸ¥è¯¢è¡¨å•
const searchForm = reactive({
  taskNo: "",
  vehicleCode: "",
  dateRange: [],
  status: "",
});
// è¡¨æ ¼æ•°æ®ï¼ˆå¸¦è¿›åº¦ç­‰è®¡ç®—字段)
const tableData = ref([]);
// ç»Ÿè®¡
const totalTasks = computed(() => rawTasks.value.length);
const finishedTasks = computed(
  () => rawTasks.value.filter((t) => t.status === "已完成").length
);
const runningTasks = computed(
  () =>
    rawTasks.value.filter((t) =>
      ["待发车", "运输中", "待签收"].includes(t.status)
    ).length
);
const completionRate = computed(() => {
  if (!totalTasks.value) return 0;
  return Math.round((finishedTasks.value / totalTasks.value) * 100);
});
// è®¡ç®—单条任务进度
const computeProgress = (task) => {
  if (task.status === "已完成" || task.signTime) return 100;
  if (task.status === "待签收" || task.deliveryTime) return 80;
  if (task.status === "运输中") return 60;
  if (task.status === "待发车" || task.loadTime) return 30;
  if (task.status === "未开始") return 0;
  return 0;
};
// çŠ¶æ€ tag æ ·å¼
const statusTagType = (status) => {
  if (status === "已完成") return "success";
  if (status === "运输中") return "warning";
  if (status === "待签收" || status === "待发车") return "info";
  return "default";
};
// é‡ç®—表格数据
const recomputeTable = () => {
  const filtered = rawTasks.value
    .filter((t) => {
      if (searchForm.taskNo && !t.taskNo.includes(searchForm.taskNo.trim())) {
        return false;
      }
      if (
        searchForm.vehicleCode &&
        !t.vehicleCode.includes(searchForm.vehicleCode.trim())
      ) {
        return false;
      }
      if (searchForm.status && t.status !== searchForm.status) {
        return false;
      }
      if (Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) {
        const [start, end] = searchForm.dateRange;
        if (!t.planDate || t.planDate < start || t.planDate > end) {
          return false;
        }
      }
      return true;
    })
    .map((t) => ({
      ...t,
      progress: computeProgress(t),
    }));
  tableData.value = filtered;
};
// æŸ¥è¯¢
const handleQuery = () => {
  recomputeTable();
};
const resetSearch = () => {
  searchForm.taskNo = "";
  searchForm.vehicleCode = "";
  searchForm.dateRange = [];
  searchForm.status = "";
  recomputeTable();
};
// è¡Œæ ·å¼
const tableRowClassName = ({ row }) => {
  if (row.status === "已完成") {
    return "row-finished";
  }
  if (row.status === "运输中") {
    return "row-running";
  }
  return "";
};
// å¼¹çª— & è¡¨å•
const dialogVisible = ref(false);
const dialogTitle = ref("新建运输任务");
const isEdit = ref(false);
const formRef = ref(null);
const form = reactive({
  id: null,
  taskNo: "",
  outboundOrderNo: "",
  vehicleCode: "",
  plateNumber: "",
  driverName: "",
  driverPhone: "",
  loadAddress: "",
  deliveryAddress: "",
  planDate: "",
  loadTime: "",
  deliveryTime: "",
  signTime: "",
  status: "未开始",
});
const rules = {
  taskNo: [{ required: true, message: "请输入任务编号", trigger: "blur" }],
  outboundOrderNo: [
    { required: true, message: "请输入出库订单号", trigger: "blur" },
  ],
  vehicleCode: [{ required: true, message: "请输入车辆编号", trigger: "blur" }],
  plateNumber: [{ required: true, message: "请输入车牌号码", trigger: "blur" }],
  driverName: [{ required: true, message: "请输入司机姓名", trigger: "blur" }],
  loadAddress: [{ required: true, message: "请输入装货地点", trigger: "blur" }],
  deliveryAddress: [
    { required: true, message: "请输入送货地点", trigger: "blur" },
  ],
  planDate: [{ required: true, message: "请选择计划日期", trigger: "change" }],
};
// æ–°å»º
const openAdd = () => {
  dialogTitle.value = "新建运输任务";
  isEdit.value = false;
  Object.assign(form, {
    id: null,
    taskNo: "",
    outboundOrderNo: "",
    vehicleCode: "",
    plateNumber: "",
    driverName: "",
    driverPhone: "",
    loadAddress: "",
    deliveryAddress: "",
    planDate: "",
    loadTime: "",
    deliveryTime: "",
    signTime: "",
    status: "未开始",
  });
  dialogVisible.value = true;
};
// ç¼–辑
const openEdit = (row) => {
  dialogTitle.value = "编辑运输任务";
  isEdit.value = true;
  Object.assign(form, row);
  dialogVisible.value = true;
};
// ä¿å­˜
const handleSubmit = () => {
  if (!formRef.value) return;
  formRef.value.validate((valid) => {
    if (!valid) return;
    if (isEdit.value) {
      const index = rawTasks.value.findIndex((t) => t.id === form.id);
      if (index !== -1) {
        rawTasks.value[index] = { ...form };
      }
      ElMessage.success("运输任务已更新");
    } else {
      const newId = rawTasks.value.length
        ? Math.max(...rawTasks.value.map((t) => t.id)) + 1
        : 1;
      rawTasks.value.push({ ...form, id: newId });
      ElMessage.success("运输任务已新增");
    }
    dialogVisible.value = false;
    recomputeTable();
  });
};
const handleCancel = () => {
  dialogVisible.value = false;
};
// åˆ é™¤
const removeRow = (row) => {
  ElMessageBox.confirm("是否确认删除该运输任务?", "删除提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      rawTasks.value = rawTasks.value.filter((t) => t.id !== row.id);
      recomputeTable();
      ElMessage.success("删除成功");
    })
    .catch(() => {});
};
onMounted(() => {
  recomputeTable();
});
</script>
<style scoped lang="scss">
.dialog-footer {
  text-align: right;
}
::v-deep(.row-finished) {
  background-color: #f6ffed;
}
::v-deep(.row-running) {
  background-color: #fffbe6;
}
</style>
src/views/inventoryManagement/vehicleFuelManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,556 @@
<template>
  <div class="app-container">
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <div class="search_form">
      <div>
        <span class="search_title">车辆编号:</span>
        <el-input
          v-model="searchForm.vehicleCode"
          style="width: 200px"
          placeholder="请输入车辆编号"
          clearable
          @keyup.enter.native="handleQuery"
        />
        <span class="search_title ml10">加油时间:</span>
        <el-date-picker
          v-model="searchForm.dateRange"
          type="daterange"
          range-separator="至"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          clearable
          @change="handleQuery"
        />
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
        <el-button type="primary" icon="Plus" @click="openAdd">
          æ–°å¢žåŠ æ²¹è®°å½•
        </el-button>
      </div>
    </div>
    <!-- è¡¨æ ¼ -->
    <div class="table_list">
      <el-table
        :data="tableData"
        border
        style="width: 100%"
        height="calc(100vh - 18.5em)"
        :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
        :row-class-name="tableRowClassName"
      >
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column
          prop="vehicleCode"
          label="车辆编号"
          width="130"
          show-overflow-tooltip
        />
        <el-table-column
          prop="plateNumber"
          label="车牌号码"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          prop="fuelDate"
          label="加油日期"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          prop="gunNo"
          label="油枪号"
          width="90"
          align="center"
        />
        <el-table-column
          prop="amount"
          label="金额(元)"
          width="100"
          align="right"
        >
          <template #default="scope">
            <span>{{ scope.row.amount?.toFixed(2) }}</span>
          </template>
        </el-table-column>
        <el-table-column
          prop="liters"
          label="升数(L)"
          width="90"
          align="right"
        >
          <template #default="scope">
            <span>{{ scope.row.liters?.toFixed(2) }}</span>
          </template>
        </el-table-column>
        <el-table-column
          prop="startMileage"
          label="起始里程(km)"
          width="120"
          align="right"
        />
        <el-table-column
          prop="endMileage"
          label="结束里程(km)"
          width="120"
          align="right"
        />
        <el-table-column
          prop="distance"
          label="行驶里程(km)"
          width="120"
          align="right"
        />
        <el-table-column
          prop="fuelConsumption"
          label="油耗(L/100km)"
          width="130"
          align="center"
        >
          <template #default="scope">
            <span
              :style="scope.row.isAbnormal ? 'color:#F56C6C;font-weight:600;' : ''"
            >
              {{ scope.row.fuelConsumption != null ? scope.row.fuelConsumption.toFixed(2) : '-' }}
            </span>
          </template>
        </el-table-column>
        <el-table-column
          prop="avgConsumption"
          label="车辆平均油耗"
          width="130"
          align="center"
        >
          <template #default="scope">
            <span>
              {{ scope.row.avgConsumption != null ? scope.row.avgConsumption.toFixed(2) : '-' }}
            </span>
          </template>
        </el-table-column>
        <el-table-column label="异常预警" width="100" align="center">
          <template #default="scope">
            <el-tag v-if="scope.row.isAbnormal" type="danger" size="small">
              å¼‚常
            </el-tag>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column label="操作" fixed="right" width="140" align="center">
          <template #default="scope">
            <el-button
              type="primary"
              link
              size="small"
              @click="openEdit(scope.row)"
            >
              ç¼–辑
            </el-button>
            <el-button
              type="danger"
              link
              size="small"
              @click="removeRow(scope.row)"
            >
              åˆ é™¤
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <!-- æ–°å¢ž/编辑弹窗 -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="640px"
      destroy-on-close
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="110px"
        label-position="right"
      >
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="车辆编号:" prop="vehicleCode">
              <el-input
                v-model="form.vehicleCode"
                placeholder="请输入车辆编号"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="车牌号码:" prop="plateNumber">
              <el-input
                v-model="form.plateNumber"
                placeholder="请输入车牌号码"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="加油日期:" prop="fuelDate">
              <el-date-picker
                v-model="form.fuelDate"
                type="date"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                placeholder="请选择加油日期"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="油枪号:" prop="gunNo">
              <el-input
                v-model="form.gunNo"
                placeholder="请输入油枪号"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="金额(元):" prop="amount">
              <el-input-number
                v-model="form.amount"
                :min="0"
                :step="0.01"
                :precision="2"
                placeholder="请输入金额"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="升数(L):" prop="liters">
              <el-input-number
                v-model="form.liters"
                :min="0"
                :step="0.01"
                :precision="2"
                placeholder="请输入升数"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="起始里程(km):" prop="startMileage">
              <el-input-number
                v-model="form.startMileage"
                :min="0"
                :step="1"
                placeholder="请输入起始里程"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="结束里程(km):" prop="endMileage">
              <el-input-number
                v-model="form.endMileage"
                :min="0"
                :step="1"
                placeholder="请输入结束里程"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="handleCancel">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="handleSubmit">保 å­˜</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
// æ¨¡æ‹ŸåŠ æ²¹è®°å½•æ•°æ®
const rawRecords = ref([
  {
    id: 1,
    vehicleCode: "CL-202401",
    plateNumber: "粤A12345",
    fuelDate: "2024-12-01",
    gunNo: "01",
    amount: 500,
    liters: 70,
    startMileage: 12000,
    endMileage: 12600,
  },
  {
    id: 2,
    vehicleCode: "CL-202401",
    plateNumber: "粤A12345",
    fuelDate: "2024-12-15",
    gunNo: "02",
    amount: 520,
    liters: 72,
    startMileage: 12600,
    endMileage: 13250,
  },
  {
    id: 3,
    vehicleCode: "CL-202402",
    plateNumber: "粤B67890",
    fuelDate: "2024-12-05",
    gunNo: "03",
    amount: 430,
    liters: 60,
    startMileage: 8000,
    endMileage: 8520,
  },
  {
    id: 4,
    vehicleCode: "CL-202402",
    plateNumber: "粤B67890",
    fuelDate: "2024-12-20",
    gunNo: "01",
    amount: 450,
    liters: 63,
    startMileage: 8520,
    endMileage: 9000,
  },
  {
    id: 5,
    vehicleCode: "CL-202401",
    plateNumber: "粤A12345",
    fuelDate: "2025-01-05",
    gunNo: "01",
    amount: 700,
    liters: 90,
    startMileage: 13250,
    endMileage: 13600, // æ˜Žæ˜¾å¼‚常油耗
  },
]);
// æŸ¥è¯¢è¡¨å•
const searchForm = reactive({
  vehicleCode: "",
  dateRange: [],
});
// è¡¨æ ¼æ•°æ®ï¼ˆåŒ…含计算字段)
const tableData = ref([]);
// å¼¹çª— & è¡¨å•
const dialogVisible = ref(false);
const dialogTitle = ref("新增加油记录");
const isEdit = ref(false);
const formRef = ref(null);
const form = reactive({
  id: null,
  vehicleCode: "",
  plateNumber: "",
  fuelDate: "",
  gunNo: "",
  amount: null,
  liters: null,
  startMileage: null,
  endMileage: null,
});
const rules = {
  vehicleCode: [{ required: true, message: "请输入车辆编号", trigger: "blur" }],
  plateNumber: [{ required: true, message: "请输入车牌号码", trigger: "blur" }],
  fuelDate: [{ required: true, message: "请选择加油日期", trigger: "change" }],
  gunNo: [{ required: true, message: "请输入油枪号", trigger: "blur" }],
  amount: [{ required: true, message: "请输入金额", trigger: "blur" }],
  liters: [{ required: true, message: "请输入升数", trigger: "blur" }],
  startMileage: [{ required: true, message: "请输入起始里程", trigger: "blur" }],
  endMileage: [{ required: true, message: "请输入结束里程", trigger: "blur" }],
};
// é‡æ–°è®¡ç®—油耗、平均油耗和异常预警
const recomputeTable = () => {
  const records = rawRecords.value;
  // 1. å…ˆæŒ‰è½¦è¾†ç»Ÿè®¡å¹³å‡æ²¹è€—
  const stats = {};
  records.forEach((r) => {
    const distance = r.endMileage - r.startMileage;
    if (distance <= 0 || !r.liters) return;
    const cons = (r.liters / distance) * 100; // L/100km
    if (!stats[r.vehicleCode]) {
      stats[r.vehicleCode] = { totalCons: 0, count: 0 };
    }
    stats[r.vehicleCode].totalCons += cons;
    stats[r.vehicleCode].count += 1;
  });
  const avgMap = {};
  Object.keys(stats).forEach((key) => {
    avgMap[key] = stats[key].totalCons / stats[key].count;
  });
  // 2. æŒ‰ç­›é€‰æ¡ä»¶è¿‡æ»¤å¹¶è¡¥å……计算字段
  const filtered = records
    .filter((r) => {
      if (
        searchForm.vehicleCode &&
        !r.vehicleCode.includes(searchForm.vehicleCode.trim())
      ) {
        return false;
      }
      if (Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) {
        const [start, end] = searchForm.dateRange;
        if (r.fuelDate < start || r.fuelDate > end) {
          return false;
        }
      }
      return true;
    })
    .map((r) => {
      const distance = r.endMileage - r.startMileage;
      const fuelConsumption =
        distance > 0 && r.liters
          ? (r.liters / distance) * 100
          : null;
      const avgConsumption =
        avgMap[r.vehicleCode] != null ? avgMap[r.vehicleCode] : null;
      const isAbnormal =
        avgConsumption != null &&
        fuelConsumption != null &&
        fuelConsumption > avgConsumption * 1.2;
      return {
        ...r,
        distance,
        fuelConsumption,
        avgConsumption,
        isAbnormal,
      };
    });
  tableData.value = filtered;
};
// æŸ¥è¯¢
const handleQuery = () => {
  recomputeTable();
};
const resetSearch = () => {
  searchForm.vehicleCode = "";
  searchForm.dateRange = [];
  recomputeTable();
};
// è¡Œæ ·å¼ï¼ˆå¼‚常高亮)
const tableRowClassName = ({ row }) => {
  if (row.isAbnormal) {
    return "row-abnormal";
  }
  return "";
};
// æ–°å¢ž
const openAdd = () => {
  dialogTitle.value = "新增加油记录";
  isEdit.value = false;
  Object.assign(form, {
    id: null,
    vehicleCode: "",
    plateNumber: "",
    fuelDate: "",
    gunNo: "",
    amount: null,
    liters: null,
    startMileage: null,
    endMileage: null,
  });
  dialogVisible.value = true;
};
// ç¼–辑
const openEdit = (row) => {
  dialogTitle.value = "编辑加油记录";
  isEdit.value = true;
  Object.assign(form, row);
  dialogVisible.value = true;
};
// ä¿å­˜
const handleSubmit = () => {
  if (!formRef.value) return;
  formRef.value.validate((valid) => {
    if (!valid) return;
    if (form.endMileage <= form.startMileage) {
      ElMessage.warning("结束里程必须大于起始里程");
      return;
    }
    if (isEdit.value) {
      const index = rawRecords.value.findIndex((r) => r.id === form.id);
      if (index !== -1) {
        rawRecords.value[index] = { ...form };
      }
      ElMessage.success("加油记录已更新");
    } else {
      const newId = rawRecords.value.length
        ? Math.max(...rawRecords.value.map((r) => r.id)) + 1
        : 1;
      rawRecords.value.push({ ...form, id: newId });
      ElMessage.success("加油记录已新增");
    }
    dialogVisible.value = false;
    recomputeTable();
  });
};
const handleCancel = () => {
  dialogVisible.value = false;
};
// åˆ é™¤
const removeRow = (row) => {
  ElMessageBox.confirm("是否确认删除该加油记录?", "删除提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      rawRecords.value = rawRecords.value.filter((r) => r.id !== row.id);
      recomputeTable();
      ElMessage.success("删除成功");
    })
    .catch(() => {});
};
onMounted(() => {
  recomputeTable();
});
</script>
<style scoped lang="scss">
.dialog-footer {
  text-align: right;
}
::v-deep(.row-abnormal) {
  background-color: #fff5f5;
}
</style>
src/views/inventoryManagement/vehicleManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,581 @@
<template>
  <div class="app-container">
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <div class="search_form">
      <div>
        <span class="search_title">车牌号码:</span>
        <el-input
          v-model="searchForm.plateNumber"
          style="width: 180px"
          placeholder="请输入车牌号码"
          clearable
          @keyup.enter.native="handleQuery"
        />
        <span class="search_title ml10">车辆类型:</span>
        <el-select
          v-model="searchForm.vehicleType"
          style="width: 160px"
          placeholder="请选择车辆类型"
          clearable
        >
          <el-option
            v-for="item in vehicleTypeOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
        <span class="search_title ml10">所属部门:</span>
        <el-select
          v-model="searchForm.department"
          style="width: 160px"
          placeholder="请选择所属部门"
          clearable
        >
          <el-option
            v-for="item in departmentOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
        <span class="search_title ml10">状态:</span>
        <el-select
          v-model="searchForm.status"
          style="width: 140px"
          placeholder="请选择状态"
          clearable
        >
          <el-option
            v-for="item in statusOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
        <span class="search_title ml10">归档状态:</span>
        <el-select
          v-model="searchForm.archived"
          style="width: 140px"
          placeholder="请选择归档状态"
          clearable
        >
          <el-option label="未归档" value="false" />
          <el-option label="已归档" value="true" />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
        <el-button type="primary" icon="Plus" @click="openAdd">新增车辆</el-button>
      </div>
    </div>
    <!-- è¡¨æ ¼ -->
    <div class="table_list">
      <el-table
        :data="tableData"
        border
        style="width: 100%"
        height="calc(100vh - 18.5em)"
        :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
      >
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column
          prop="vehicleCode"
          label="车辆编号"
          width="140"
          show-overflow-tooltip
        />
        <el-table-column
          prop="plateNumber"
          label="车牌号码"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          prop="vehicleType"
          label="车辆类型"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          prop="department"
          label="所属部门"
          width="140"
          show-overflow-tooltip
        />
        <el-table-column
          prop="purchaseDate"
          label="购置日期"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          prop="licenseNumber"
          label="行驶证编号"
          width="160"
          show-overflow-tooltip
        />
        <el-table-column
          prop="licenseIssueDate"
          label="发证日期"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          prop="licenseExpireDate"
          label="到期日期"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column label="状态" width="100" align="center">
          <template #default="scope">
            <el-tag :type="statusTagType(scope.row.status)">
              {{ scope.row.status }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="归档状态" width="100" align="center">
          <template #default="scope">
            <el-tag :type="scope.row.archived ? 'info' : 'success'">
              {{ scope.row.archived ? '已归档' : '未归档' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" fixed="right" width="220" align="center">
          <template #default="scope">
            <el-button
              type="primary"
              link
              size="small"
              @click="openEdit(scope.row)"
            >
              ç¼–辑
            </el-button>
            <el-button
              type="warning"
              link
              size="small"
              :disabled="scope.row.archived"
              @click="archiveRow(scope.row)"
            >
              å½’æ¡£
            </el-button>
            <el-button
              type="danger"
              link
              size="small"
              @click="removeRow(scope.row)"
            >
              åˆ é™¤
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <!-- æ–°å¢ž/编辑弹窗 -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="600px"
      destroy-on-close
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="100px"
        label-position="right"
      >
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="车辆编号:" prop="vehicleCode">
              <el-input v-model="form.vehicleCode" placeholder="请输入车辆编号" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="车牌号码:" prop="plateNumber">
              <el-input v-model="form.plateNumber" placeholder="请输入车牌号码" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="车辆类型:" prop="vehicleType">
              <el-select
                v-model="form.vehicleType"
                placeholder="请选择车辆类型"
                clearable
              >
                <el-option
                  v-for="item in vehicleTypeOptions"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="所属部门:" prop="department">
              <el-select
                v-model="form.department"
                placeholder="请选择所属部门"
                clearable
              >
                <el-option
                  v-for="item in departmentOptions"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="购置日期:" prop="purchaseDate">
              <el-date-picker
                v-model="form.purchaseDate"
                type="date"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                placeholder="请选择购置日期"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="状态:" prop="status">
              <el-select v-model="form.status" placeholder="请选择状态">
                <el-option
                  v-for="item in statusOptions"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="行驶证编号:" prop="licenseNumber">
              <el-input
                v-model="form.licenseNumber"
                placeholder="请输入行驶证编号"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="发证日期:" prop="licenseIssueDate">
              <el-date-picker
                v-model="form.licenseIssueDate"
                type="date"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                placeholder="请选择发证日期"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="到期日期:" prop="licenseExpireDate">
              <el-date-picker
                v-model="form.licenseExpireDate"
                type="date"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                placeholder="请选择到期日期"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="handleCancel">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="handleSubmit">保 å­˜</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
// æ¨¡æ‹Ÿè½¦è¾†åŸºç¡€æ•°æ®
const allVehicles = ref([
  {
    id: 1,
    vehicleCode: "CL-202401",
    plateNumber: "粤A12345",
    vehicleType: "厢式货车",
    department: "物流一部",
    purchaseDate: "2022-03-15",
    licenseNumber: "4401-2022-0001",
    licenseIssueDate: "2022-03-10",
    licenseExpireDate: "2026-03-10",
    status: "在用",
    archived: false,
  },
  {
    id: 2,
    vehicleCode: "CL-202402",
    plateNumber: "粤B67890",
    vehicleType: "冷藏车",
    department: "物流二部",
    purchaseDate: "2021-08-01",
    licenseNumber: "4401-2021-0123",
    licenseIssueDate: "2021-07-28",
    licenseExpireDate: "2025-07-28",
    status: "ç»´ä¿®",
    archived: false,
  },
  {
    id: 3,
    vehicleCode: "CL-202403",
    plateNumber: "粤C11223",
    vehicleType: "牵引车",
    department: "项目运输部",
    purchaseDate: "2020-05-20",
    licenseNumber: "4401-2020-0456",
    licenseIssueDate: "2020-05-18",
    licenseExpireDate: "2024-05-18",
    status: "闲置",
    archived: false,
  },
  {
    id: 4,
    vehicleCode: "CL-202404",
    plateNumber: "粤D33445",
    vehicleType: "厢式货车",
    department: "资产管理部",
    purchaseDate: "2019-11-11",
    licenseNumber: "4401-2019-0789",
    licenseIssueDate: "2019-11-08",
    licenseExpireDate: "2023-11-08",
    status: "在用",
    archived: true,
  },
]);
// ä¸‹æ‹‰æžšä¸¾
const vehicleTypeOptions = [
  { label: "厢式货车", value: "厢式货车" },
  { label: "冷藏车", value: "冷藏车" },
  { label: "牵引车", value: "牵引车" },
  { label: "其他", value: "其他" },
];
const departmentOptions = [
  { label: "物流一部", value: "物流一部" },
  { label: "物流二部", value: "物流二部" },
  { label: "项目运输部", value: "项目运输部" },
  { label: "资产管理部", value: "资产管理部" },
];
const statusOptions = [
  { label: "在用", value: "在用" },
  { label: "闲置", value: "闲置" },
  { label: "ç»´ä¿®", value: "ç»´ä¿®" },
];
// æŸ¥è¯¢è¡¨å•
const searchForm = reactive({
  plateNumber: "",
  vehicleType: "",
  department: "",
  status: "",
  archived: "",
});
// è¡¨æ ¼æ•°æ®
const tableData = ref([...allVehicles.value]);
// å¼¹çª— & è¡¨å•
const dialogVisible = ref(false);
const dialogTitle = ref("新增车辆");
const isEdit = ref(false);
const formRef = ref(null);
const form = reactive({
  id: null,
  vehicleCode: "",
  plateNumber: "",
  vehicleType: "",
  department: "",
  purchaseDate: "",
  licenseNumber: "",
  licenseIssueDate: "",
  licenseExpireDate: "",
  status: "在用",
  archived: false,
});
const rules = {
  vehicleCode: [{ required: true, message: "请输入车辆编号", trigger: "blur" }],
  plateNumber: [{ required: true, message: "请输入车牌号码", trigger: "blur" }],
  vehicleType: [{ required: true, message: "请选择车辆类型", trigger: "change" }],
  department: [{ required: true, message: "请选择所属部门", trigger: "change" }],
  purchaseDate: [{ required: true, message: "请选择购置日期", trigger: "change" }],
  status: [{ required: true, message: "请选择状态", trigger: "change" }],
  licenseNumber: [{ required: true, message: "请输入行驶证编号", trigger: "blur" }],
  licenseIssueDate: [{ required: true, message: "请选择发证日期", trigger: "change" }],
};
// æŸ¥è¯¢
const handleQuery = () => {
  tableData.value = allVehicles.value.filter((item) => {
    if (
      searchForm.plateNumber &&
      !item.plateNumber.includes(searchForm.plateNumber.trim())
    ) {
      return false;
    }
    if (searchForm.vehicleType && item.vehicleType !== searchForm.vehicleType) {
      return false;
    }
    if (searchForm.department && item.department !== searchForm.department) {
      return false;
    }
    if (searchForm.status && item.status !== searchForm.status) {
      return false;
    }
    if (searchForm.archived !== "") {
      const targetArchived = searchForm.archived === "true";
      if (item.archived !== targetArchived) return false;
    }
    return true;
  });
};
const resetSearch = () => {
  searchForm.plateNumber = "";
  searchForm.vehicleType = "";
  searchForm.department = "";
  searchForm.status = "";
  searchForm.archived = "";
  handleQuery();
};
// æ–°å¢ž
const openAdd = () => {
  dialogTitle.value = "新增车辆";
  isEdit.value = false;
  Object.assign(form, {
    id: null,
    vehicleCode: "",
    plateNumber: "",
    vehicleType: "",
    department: "",
    purchaseDate: "",
    licenseInfo: "",
    status: "在用",
    archived: false,
  });
  dialogVisible.value = true;
};
// ç¼–辑
const openEdit = (row) => {
  dialogTitle.value = "编辑车辆";
  isEdit.value = true;
  Object.assign(form, row);
  dialogVisible.value = true;
};
// ä¿å­˜
const handleSubmit = () => {
  if (!formRef.value) return;
  formRef.value.validate((valid) => {
    if (!valid) return;
    if (isEdit.value) {
      const index = allVehicles.value.findIndex((v) => v.id === form.id);
      if (index !== -1) {
        allVehicles.value[index] = { ...form };
      }
      ElMessage.success("车辆信息已更新");
    } else {
      const newId = allVehicles.value.length
        ? Math.max(...allVehicles.value.map((v) => v.id)) + 1
        : 1;
      allVehicles.value.push({ ...form, id: newId });
      ElMessage.success("车辆信息已新增");
    }
    dialogVisible.value = false;
    handleQuery();
  });
};
const handleCancel = () => {
  dialogVisible.value = false;
};
// å½’æ¡£
const archiveRow = (row) => {
  if (row.archived) return;
  ElMessageBox.confirm(
    "是否确认将该车辆归档?归档后仅保留查询,不再参与运输任务分配。",
    "归档提示",
    {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    }
  )
    .then(() => {
      row.archived = true;
      if (row.status === "在用") {
        row.status = "闲置";
      }
      ElMessage.success("车辆已归档");
      handleQuery();
    })
    .catch(() => {});
};
// åˆ é™¤
const removeRow = (row) => {
  ElMessageBox.confirm("是否确认删除该车辆基础信息?", "删除提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      allVehicles.value = allVehicles.value.filter((v) => v.id !== row.id);
      handleQuery();
      ElMessage.success("删除成功");
    })
    .catch(() => {});
};
// çŠ¶æ€æ ·å¼
const statusTagType = (status) => {
  if (status === "在用") return "success";
  if (status === "闲置") return "info";
  if (status === "ç»´ä¿®") return "warning";
  return "default";
};
</script>
<style scoped lang="scss">
.dialog-footer {
  text-align: right;
}
</style>
src/views/personnelManagement/attendanceCheckin/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,469 @@
<template>
  <div class="app-container">
    <!-- å‘˜å·¥æ‰“卡区 -->
    <el-card shadow="never" class="mb16">
      <div class="attendance-header">
        <div>
          <div class="title">打卡签到</div>
          <div class="sub-title">支持一键打卡,自动记录上下班时间</div>
        </div>
        <div class="attendance-actions">
          <div class="time-block">
            <div class="label">当前时间</div>
            <div class="value">{{ nowTime }}</div>
          </div>
          <el-button type="primary" size="large" @click="handleCheckInOut">
            {{ checkInOutText }}
          </el-button>
        </div>
      </div>
      <el-descriptions border :column="4" class="mt10">
        <el-descriptions-item label="员工姓名">
          {{ currentUser.name }}
        </el-descriptions-item>
        <el-descriptions-item label="工号">
          {{ currentUser.no }}
        </el-descriptions-item>
        <el-descriptions-item label="所属部门">
          {{ currentUser.dept }}
        </el-descriptions-item>
        <el-descriptions-item label="今日状态">
          <el-tag :type="todayStatusTag" size="small">
            {{ todayStatusText }}
          </el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="上班时间">
          {{ todayRecord?.checkInTime || '-' }}
        </el-descriptions-item>
        <el-descriptions-item label="下班时间">
          {{ todayRecord?.checkOutTime || '-' }}
        </el-descriptions-item>
        <el-descriptions-item label="工时(小时)">
          {{ todayRecord?.workHours ?? '-' }}
        </el-descriptions-item>
        <el-descriptions-item label="异常标记">
          <span v-if="todayRecord?.status === 'normal'">-</span>
          <el-tag v-else type="danger" size="small">
            {{ todayRecord?.statusText }}
          </el-tag>
        </el-descriptions-item>
      </el-descriptions>
    </el-card>
    <!-- æŸ¥è¯¢æ¡ä»¶ï¼ˆç®¡ç†å‘˜è€ƒå‹¤æ—¥æŠ¥ï¼‰ -->
    <div class="search_form">
      <div>
        <span class="search_title">部门:</span>
        <el-select
          v-model="searchForm.dept"
          placeholder="请选择部门"
          style="width: 180px"
          clearable
        >
          <el-option
            v-for="item in deptOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
        <span class="search_title ml10">日期:</span>
        <el-date-picker
          v-model="searchForm.date"
          type="date"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          placeholder="请选择日期"
          clearable
        />
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
        <el-button icon="Download" @click="handleExport">
          å¯¼å‡ºè€ƒå‹¤æ—¥æŠ¥
        </el-button>
      </div>
    </div>
    <!-- è€ƒå‹¤æ—¥æŠ¥è¡¨æ ¼ -->
    <div class="table_list">
      <el-table
        :data="tableData"
        border
        style="width: 100%"
        height="calc(100vh - 24em)"
        :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
        :row-class-name="rowClassName"
      >
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column
          prop="date"
          label="日期"
          width="120"
        />
        <el-table-column
          prop="dept"
          label="部门"
          width="140"
        />
        <el-table-column
          prop="name"
          label="姓名"
          width="120"
        />
        <el-table-column
          prop="no"
          label="工号"
          width="120"
        />
        <el-table-column
          prop="checkInTime"
          label="上班时间"
          width="140"
        />
        <el-table-column
          prop="checkOutTime"
          label="下班时间"
          width="140"
        />
        <el-table-column
          prop="workHours"
          label="工时(小时)"
          width="110"
          align="center"
        />
        <el-table-column
          prop="statusText"
          label="考勤状态"
          width="120"
          align="center"
        >
          <template #default="scope">
            <el-tag
              v-if="scope.row.status === 'normal'"
              type="success"
              size="small"
            >
              æ­£å¸¸
            </el-tag>
            <el-tag
              v-else
              type="danger"
              size="small"
            >
              {{ scope.row.statusText }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column
          prop="remark"
          label="备注"
          show-overflow-tooltip
        />
      </el-table>
    </div>
  </div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue";
import { ElMessage } from "element-plus";
// æ¨¡æ‹Ÿå½“前登录员工
const currentUser = reactive({
  id: 1,
  name: "张三",
  no: "E10001",
  dept: "生产一部",
});
// éƒ¨é—¨é€‰é¡¹
const deptOptions = [
  { label: "生产一部", value: "生产一部" },
  { label: "生产二部", value: "生产二部" },
  { label: "设备维护部", value: "设备维护部" },
  { label: "质检部", value: "质检部" },
];
// æ¨¡æ‹Ÿè€ƒå‹¤åŽŸå§‹æ•°æ®
const rawAttendance = ref([
  {
    id: 1,
    date: "2024-12-01",
    userId: 1,
    name: "张三",
    no: "E10001",
    dept: "生产一部",
    checkInTime: "08:58",
    checkOutTime: "18:10",
    workHours: 9.2,
    status: "normal",
    statusText: "正常",
    remark: "",
  },
  {
    id: 2,
    date: "2024-12-01",
    userId: 2,
    name: "李四",
    no: "E10002",
    dept: "生产一部",
    checkInTime: "09:15",
    checkOutTime: "18:05",
    workHours: 8.8,
    status: "late",
    statusText: "迟到",
    remark: "因交通拥堵迟到",
  },
  {
    id: 3,
    date: "2024-12-01",
    userId: 3,
    name: "王五",
    no: "E20001",
    dept: "设备维护部",
    checkInTime: "08:50",
    checkOutTime: "17:20",
    workHours: 8.5,
    status: "early",
    statusText: "早退",
    remark: "外出处理紧急故障",
  },
  {
    id: 4,
    date: "2024-12-02",
    userId: 1,
    name: "张三",
    no: "E10001",
    dept: "生产一部",
    checkInTime: "08:45",
    checkOutTime: "18:30",
    workHours: 9.7,
    status: "normal",
    statusText: "正常",
    remark: "加班0.5小时",
  },
]);
// æŸ¥è¯¢è¡¨å•
const searchForm = reactive({
  dept: "",
  date: "",
});
// è¡¨æ ¼æ•°æ®
const tableData = ref([]);
// å½“前时间展示
const nowTime = ref("");
let timer = null;
const updateNowTime = () => {
  const now = new Date();
  const Y = now.getFullYear();
  const M = String(now.getMonth() + 1).padStart(2, "0");
  const D = String(now.getDate()).padStart(2, "0");
  const h = String(now.getHours()).padStart(2, "0");
  const m = String(now.getMinutes()).padStart(2, "0");
  const s = String(now.getSeconds()).padStart(2, "0");
  nowTime.value = `${Y}-${M}-${D} ${h}:${m}:${s}`;
};
// ä»Šæ—¥æ—¥æœŸ
const todayStr = computed(() => nowTime.value.slice(0, 10));
// å½“日当前员工考勤记录
const todayRecord = computed(() =>
  rawAttendance.value.find(
    (item) =>
      item.userId === currentUser.id && item.date === todayStr.value
  )
);
// æ‰“卡按钮文本
const checkInOutText = computed(() => {
  if (!todayRecord.value || !todayRecord.value.checkInTime) {
    return "上班打卡";
  }
  if (!todayRecord.value.checkOutTime) {
    return "下班打卡";
  }
  return "今日已打卡完成";
});
// ä»Šæ—¥çŠ¶æ€å±•ç¤º
const todayStatusTag = computed(() => {
  if (!todayRecord.value) return "info";
  if (todayRecord.value.status === "normal") return "success";
  return "danger";
});
const todayStatusText = computed(() => {
  if (!todayRecord.value) return "未打卡";
  return todayRecord.value.statusText || "正常";
});
// è¡Œæ ·å¼ï¼šå¼‚常高亮
const rowClassName = ({ row }) => {
  if (row.status === "late" || row.status === "early") {
    return "row-abnormal";
  }
  return "";
};
// æŸ¥è¯¢
const recomputeTable = () => {
  const list = rawAttendance.value.filter((item) => {
    if (searchForm.dept && item.dept !== searchForm.dept) {
      return false;
    }
    if (searchForm.date && item.date !== searchForm.date) {
      return false;
    }
    return true;
  });
  tableData.value = list;
};
const handleQuery = () => {
  recomputeTable();
};
const resetSearch = () => {
  searchForm.dept = "";
  searchForm.date = "";
  recomputeTable();
};
// å¯¼å‡ºï¼ˆæ¼”示)
const handleExport = () => {
  ElMessage.success("当前为演示页面,导出功能未对接实际接口");
};
// æ‰“卡逻辑(仅前端模拟)
const handleCheckInOut = () => {
  const [dateStr, timeStr] = nowTime.value.split(" ");
  if (!dateStr || !timeStr) return;
  // ä¸Šç­æ‰“卡
  if (!todayRecord.value) {
    const newId = rawAttendance.value.length
      ? Math.max(...rawAttendance.value.map((i) => i.id)) + 1
      : 1;
    const status =
      timeStr > "09:00:00" ? "late" : "normal";
    const statusText = status === "late" ? "迟到" : "正常";
    rawAttendance.value.push({
      id: newId,
      date: dateStr,
      userId: currentUser.id,
      name: currentUser.name,
      no: currentUser.no,
      dept: currentUser.dept,
      checkInTime: timeStr.slice(0, 5),
      checkOutTime: "",
      workHours: null,
      status,
      statusText,
      remark: "",
    });
    ElMessage.success("上班打卡成功");
  } else if (!todayRecord.value.checkOutTime) {
    // ä¸‹ç­æ‰“卡
    todayRecord.value.checkOutTime = timeStr.slice(0, 5);
    // ç®€å•按 9:00-18:00 è®¡ç®—工时
    const start = todayRecord.value.checkInTime || "09:00";
    const [sh, sm] = start.split(":").map((v) => parseInt(v, 10));
    const [eh, em] = todayRecord.value.checkOutTime
      .split(":")
      .map((v) => parseInt(v, 10));
    const diff = (eh * 60 + em - (sh * 60 + sm)) / 60;
    todayRecord.value.workHours = Number(Math.max(diff, 0).toFixed(1));
    // æ—©é€€åˆ¤æ–­ï¼š18:00 å‰ç¦»å¼€è§†ä¸ºæ—©é€€ï¼ˆåªç¤ºæ„ï¼‰
    if (timeStr < "18:00:00") {
      todayRecord.value.status = "early";
      todayRecord.value.statusText = "早退";
    } else if (todayRecord.value.status === "normal") {
      todayRecord.value.statusText = "正常";
    }
    ElMessage.success("下班打卡成功");
  } else {
    ElMessage.info("今日已完成上下班打卡");
  }
  recomputeTable();
};
onMounted(() => {
  updateNowTime();
  timer = setInterval(updateNowTime, 1000);
  // é»˜è®¤å±•示当天数据
  const today = new Date();
  const Y = today.getFullYear();
  const M = String(today.getMonth() + 1).padStart(2, "0");
  const D = String(today.getDate()).padStart(2, "0");
  searchForm.date = `${Y}-${M}-${D}`;
  recomputeTable();
});
onBeforeUnmount(() => {
  if (timer) {
    clearInterval(timer);
  }
});
</script>
<style scoped lang="scss">
.mb16 {
  margin-bottom: 16px;
}
.attendance-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.attendance-header .title {
  font-size: 18px;
  font-weight: 600;
  margin-bottom: 4px;
}
.attendance-header .sub-title {
  font-size: 13px;
  color: #909399;
}
.attendance-actions {
  display: flex;
  align-items: center;
  gap: 16px;
}
.time-block {
  text-align: right;
}
.time-block .label {
  font-size: 12px;
  color: #909399;
}
.time-block .value {
  font-size: 18px;
  font-weight: 600;
  color: #333;
}
::v-deep(.row-abnormal) {
  background-color: #fff5f5;
}
</style>
src/views/qualityManagement/afterSalesTraceability/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,595 @@
<template>
  <div class="app-container">
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <el-form
      :model="queryParams"
      ref="queryForm"
      :inline="true"
      v-show="showSearch"
      label-width="90px"
    >
      <el-form-item label="产品型号" prop="productModel">
        <el-input
          v-model="queryParams.productModel"
          placeholder="请输入产品型号"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="客户名称" prop="customerName">
        <el-input
          v-model="queryParams.customerName"
          placeholder="请输入客户名称"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="反馈时间" prop="feedbackRange">
        <el-date-picker
          v-model="queryParams.feedbackRange"
          type="daterange"
          range-separator="至"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          clearable
        />
      </el-form-item>
      <el-form-item label="处理状态" prop="status">
        <el-select
          v-model="queryParams.status"
          placeholder="请选择处理状态"
          clearable
        >
          <el-option
            v-for="item in statusOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="Search" @click="handleQuery">
          æœç´¢
        </el-button>
        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <!-- æ“ä½œåŒº -->
    <el-row :gutter="10" class="mb8">
      <el-col :span="3">
        <el-button
          type="primary"
          plain
          icon="Plus"
          @click="handleAdd"
        >
          æ–°å¢žå”®åŽè´¨é‡è®°å½•
        </el-button>
      </el-col>
      <el-col :span="3">
        <el-button
          type="success"
          plain
          icon="Edit"
          :disabled="single"
          @click="handleUpdate"
        >
          ä¿®æ”¹
        </el-button>
      </el-col>
      <el-col :span="3">
        <el-button
          type="danger"
          plain
          icon="Delete"
          :disabled="multiple"
          @click="handleDelete"
        >
          åˆ é™¤
        </el-button>
      </el-col>
      <el-col :span="3">
        <el-button
          type="warning"
          plain
          icon="Download"
          @click="handleExport"
        >
          å¯¼å‡º
        </el-button>
      </el-col>
      <right-toolbar
        v-model:showSearch="showSearch"
        @queryTable="getList"
      />
    </el-row>
    <!-- æ•°æ®è¡¨ -->
    <el-table
      v-loading="loading"
      :data="afterSalesList"
      @selection-change="handleSelectionChange"
    >
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column label="序号" type="index" width="55" align="center" />
      <el-table-column label="销售合同号" prop="contractNo" width="160" />
      <el-table-column label="产品编号" prop="productCode" width="140" />
      <el-table-column label="产品型号" prop="productModel" width="140" />
      <el-table-column label="客户名称" prop="customerName" width="160" />
      <el-table-column label="联系方式" prop="contact" width="140" />
      <el-table-column label="反馈时间" prop="feedbackTime" width="160">
        <template #default="scope">
          <span>{{ scope.row.feedbackTime }}</span>
        </template>
      </el-table-column>
      <el-table-column label="问题描述" prop="problemDesc" show-overflow-tooltip />
      <el-table-column label="维修情况" prop="repairInfo" show-overflow-tooltip />
      <el-table-column label="处理结果" prop="result" show-overflow-tooltip />
      <el-table-column label="处理状态" prop="status" width="120" align="center">
        <template #default="scope">
          <dict-tag
            :options="statusOptions"
            :value="scope.row.status"
          />
        </template>
      </el-table-column>
      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160" fixed="right">
        <template #default="scope">
          <el-button size="small" type="text" icon="Edit" @click="handleUpdate(scope.row)">
            ä¿®æ”¹
          </el-button>
          <el-button size="small" type="text" icon="Delete" @click="handleDelete(scope.row)">
            åˆ é™¤
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <pagination
      v-show="total > 0"
      :total="total"
      v-model:page="queryParams.pageNum"
      v-model:limit="queryParams.pageSize"
      @pagination="getList"
    />
    <!-- æ–°å¢ž/修改弹窗 -->
    <el-dialog
      :title="title"
      v-model="open"
      width="900px"
      append-to-body
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="110px"
      >
        <el-row>
          <el-col :span="12">
            <el-form-item label="销售合同号" prop="contractNo">
              <el-input
                v-model="form.contractNo"
                placeholder="请选择关联销售合同号"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="产品编号" prop="productCode">
              <el-input
                v-model="form.productCode"
                placeholder="请选择关联产品编号"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="产品型号" prop="productModel">
              <el-input
                v-model="form.productModel"
                placeholder="请输入产品型号"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="客户名称" prop="customerName">
              <el-input
                v-model="form.customerName"
                placeholder="请输入客户名称"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="联系方式" prop="contact">
              <el-input
                v-model="form.contact"
                placeholder="请输入客户联系方式"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="反馈时间" prop="feedbackTime">
              <el-date-picker
                v-model="form.feedbackTime"
                type="datetime"
                value-format="YYYY-MM-DD HH:mm:ss"
                format="YYYY-MM-DD HH:mm"
                placeholder="请选择反馈时间"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item label="客户反馈问题" prop="problemDesc">
              <el-input
                v-model="form.problemDesc"
                type="textarea"
                :rows="3"
                placeholder="请详细记录客户反馈问题、现象描述等信息"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="维修情况" prop="repairInfo">
              <el-input
                v-model="form.repairInfo"
                type="textarea"
                :rows="2"
                placeholder="记录维修过程、使用备件、返修次数等"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="处理结果" prop="result">
              <el-input
                v-model="form.result"
                type="textarea"
                :rows="2"
                placeholder="记录最终处理结果,如更换、退货、升级等"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="处理状态" prop="status">
              <el-select
                v-model="form.status"
                placeholder="请选择处理状态"
              >
                <el-option
                  v-for="item in statusOptions"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="备注" prop="remark">
              <el-input
                v-model="form.remark"
                type="textarea"
                :rows="2"
                placeholder="可记录后续跟踪意见、复盘结论等"
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
          <el-button @click="cancel">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup name="AfterSalesTraceability">
import { ref, reactive, onMounted } from "vue";
import { ElMessageBox } from "element-plus";
const { proxy } = getCurrentInstance();
// çŠ¶æ€å­—å…¸
const statusOptions = ref([
  { label: "待处理", value: "0" },
  { label: "处理中", value: "1" },
  { label: "已完成", value: "2" },
  { label: "已关闭", value: "3" },
]);
// æ¨¡æ‹Ÿå”®åŽè´¨é‡æ•°æ®
const afterSalesList = ref([
  {
    id: 1,
    contractNo: "SC-2024-001",
    productCode: "P-10001",
    productModel: "XG-500A",
    customerName: "华南电子科技有限公司",
    contact: "å¼ å·¥ / 13800000001",
    feedbackTime: "2024-12-01 10:23:00",
    problemDesc: "使用三个月后出现间歇性掉电,影响产线稳定运行。",
    repairInfo: "安排工程师上门检修,更换电源模块并加固接线端子。",
    result: "更换电源总成,恢复正常使用,建议客户增加UPS保护。",
    status: "2",
    remark: "列入重点跟踪客户,后续观察一个季度。",
  },
  {
    id: 2,
    contractNo: "SC-2024-015",
    productCode: "P-10045",
    productModel: "XG-500B",
    customerName: "华东精密制造有限公司",
    contact: "李工 / 13800000002",
    feedbackTime: "2024-12-05 15:40:00",
    problemDesc: "部分批次出现外壳刮花,客户投诉外观质量不达标。",
    repairInfo: "与生产现场核查,确认来料搬运及包装环节存在磕碰风险。",
    result: "对问题批次重新返工,补发良品,并优化包装防护方案。",
    status: "1",
    remark: "需跟踪后续批次投诉率变化。",
  },
  {
    id: 3,
    contractNo: "SC-2024-032",
    productCode: "P-10110",
    productModel: "XG-600C",
    customerName: "西南新能源科技股份",
    contact: "王工 / 13800000003",
    feedbackTime: "2024-11-28 09:15:00",
    problemDesc: "现场调试时发现接口不兼容,需要适配客户旧版系统。",
    repairInfo: "远程技术支持+现场工程师联合排查,提供过渡适配方案。",
    result: "通过更换接插件并升级固件版本解决。",
    status: "0",
    remark: "建议下次合同前置沟通接口规格。",
  },
]);
const open = ref(false);
const loading = ref(false);
const showSearch = ref(true);
const ids = ref([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(3);
const title = ref("新增售后质量记录");
const data = reactive({
  form: {},
  queryParams: {
    pageNum: 1,
    pageSize: 10,
    productModel: null,
    customerName: null,
    feedbackRange: [],
    status: null,
  },
  rules: {
    contractNo: [
      { required: true, message: "销售合同号不能为空", trigger: "blur" },
    ],
    productCode: [
      { required: true, message: "产品编号不能为空", trigger: "blur" },
    ],
    productModel: [
      { required: true, message: "产品型号不能为空", trigger: "blur" },
    ],
    customerName: [
      { required: true, message: "客户名称不能为空", trigger: "blur" },
    ],
    contact: [
      { required: true, message: "联系方式不能为空", trigger: "blur" },
    ],
    feedbackTime: [
      { required: true, message: "反馈时间不能为空", trigger: "change" },
    ],
    problemDesc: [
      { required: true, message: "客户反馈问题不能为空", trigger: "blur" },
    ],
    status: [
      { required: true, message: "处理状态不能为空", trigger: "change" },
    ],
  },
});
const { queryParams, form, rules } = toRefs(data);
// æŸ¥è¯¢åˆ—表(仅前端筛选,不调接口)
function getList() {
  loading.value = true;
  const list = afterSalesList.value.filter((item) => {
    if (
      queryParams.value.productModel &&
      !item.productModel
        ?.toLowerCase()
        .includes(queryParams.value.productModel.toLowerCase())
    ) {
      return false;
    }
    if (
      queryParams.value.customerName &&
      !item.customerName
        ?.toLowerCase()
        .includes(queryParams.value.customerName.toLowerCase())
    ) {
      return false;
    }
    if (queryParams.value.status && item.status !== queryParams.value.status) {
      return false;
    }
    if (
      Array.isArray(queryParams.value.feedbackRange) &&
      queryParams.value.feedbackRange.length === 2
    ) {
      const [start, end] = queryParams.value.feedbackRange;
      const dateStr = item.feedbackTime?.slice(0, 10);
      if (dateStr < start || dateStr > end) {
        return false;
      }
    }
    return true;
  });
  total.value = list.length;
  // æ­¤å¤„未做分页,仅模拟全量展示
  afterSalesList.value = list;
  loading.value = false;
}
// å–消
function cancel() {
  open.value = false;
  reset();
}
// è¡¨å•重置
function reset() {
  form.value = {
    id: null,
    contractNo: null,
    productCode: null,
    productModel: null,
    customerName: null,
    contact: null,
    feedbackTime: null,
    problemDesc: null,
    repairInfo: null,
    result: null,
    status: "0",
    remark: null,
  };
  proxy.resetForm("formRef");
}
// æœç´¢
function handleQuery() {
  queryParams.value.pageNum = 1;
  getList();
}
// é‡ç½®
function resetQuery() {
  proxy.resetForm("queryForm");
  queryParams.value.feedbackRange = [];
  handleQuery();
}
// å¤šé€‰
function handleSelectionChange(selection) {
  ids.value = selection.map((item) => item.id);
  single.value = selection.length !== 1;
  multiple.value = !selection.length;
}
// æ–°å¢ž
function handleAdd() {
  reset();
  open.value = true;
  title.value = "新增售后质量记录";
}
// ä¿®æ”¹
function handleUpdate(row) {
  reset();
  const current = row || afterSalesList.value.find((item) => item.id === ids.value[0]);
  if (current) {
    form.value = { ...current };
    open.value = true;
    title.value = "修改售后质量记录";
  }
}
// æäº¤
function submitForm() {
  proxy.$refs["formRef"].validate((valid) => {
    if (valid) {
      if (form.value.id != null) {
        // ä¿®æ”¹ï¼šæ›¿æ¢æœ¬åœ°æ¨¡æ‹Ÿæ•°æ®
        const index = afterSalesList.value.findIndex(
          (item) => item.id === form.value.id
        );
        if (index !== -1) {
          afterSalesList.value.splice(index, 1, { ...form.value });
        }
        proxy.$modal.msgSuccess("修改成功");
      } else {
        // æ–°å¢žï¼šæ’入本地模拟数据
        const newId =
          afterSalesList.value.length > 0
            ? Math.max(...afterSalesList.value.map((i) => i.id)) + 1
            : 1;
        afterSalesList.value.push({ ...form.value, id: newId });
        proxy.$modal.msgSuccess("新增成功");
      }
      open.value = false;
      getList();
    }
  });
}
// åˆ é™¤
function handleDelete(row) {
  const deleteIds = row?.id ? [row.id] : ids.value;
  if (!deleteIds || deleteIds.length === 0) {
    proxy.$modal.msgWarning("请先选择要删除的记录");
    return;
  }
  ElMessageBox.confirm(
    '是否确认删除选中的售后质量记录?',
    "警告",
    {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }
  )
    .then(() => {
      // å‰ç«¯åˆ é™¤æœ¬åœ°æ¨¡æ‹Ÿæ•°æ®
      afterSalesList.value = afterSalesList.value.filter(
        (item) => !deleteIds.includes(item.id)
      );
      proxy.$modal.msgSuccess("删除成功");
      getList();
    })
    .catch(() => {});
}
// å¯¼å‡º
function handleExport() {
  proxy.$modal.msgSuccess("导出功能为演示功能,当前未对接实际导出接口");
}
onMounted(() => {
  getList();
});
</script>
<style scoped>
.mb8 {
  margin-bottom: 8px;
}
.dialog-footer {
  text-align: right;
}
</style>