ebd0ee3aa6845ded41adaa5e8eb032b7dc3575ce..53507e9d94f9229a2dbad799df7e52aa1468740c
78 分钟以前 gongchunyi
chore: 宏腾新型环保测试环境配置文件
53507e 对比 | 目录
78 分钟以前 gongchunyi
chore: 清除多余logo
bd1bca 对比 | 目录
85 分钟以前 gongchunyi
Merge branch 'dev_NEW_pro' into dev_pro_山西长治
e9a0f1 对比 | 目录
昨天 gaoluyang
pro 1.克拉玛依市新聚工贸有限责任公司打包 2.克拉玛依融邦环保科技有限公司打包
20ba93 对比 | 目录
昨天 gaoluyang
pro 1.克拉玛依市新聚工贸有限责任公司打包 2.克拉玛依融邦环保科技有限公司打包
f050e7 对比 | 目录
7 天以前 spring
fix: 合并冲突
4801ae 对比 | 目录
7 天以前 spring
fix: 合并
f32444 对比 | 目录
7 天以前 spring
Merge branch 'dev_NEW_pro' of http://114.132.189.42:9002/r/product-inventor...
c31b03 对比 | 目录
7 天以前 spring
fix: 修改山西坤源化工有限公司部署地址
dd656b 对比 | 目录
7 天以前 gaoluyang
pro 1.logo展示修改
2339ff 对比 | 目录
7 天以前
feat(config): 添加OLD环境配置
091154 对比 | 目录
8 天以前 zhang_nuo
修改logo名称
5984aa 对比 | 目录
8 天以前 zhang_nuo
配置 天津市武清区环境工程设备有限公司 天津市奇缘恒艺印刷制版有限公司
b0836c 对比 | 目录
2026-07-08 huminmin
已进行排班并且人员已打卡的前提下,对应的班次配置不能编辑删除。
28fc65 对比 | 目录
2026-07-07 gaoluyang
pro 1.去掉logo
db38a9 对比 | 目录
2026-07-07 gaoluyang
pro 1.去掉logo
99001d 对比 | 目录
2026-07-07 huminmin
排班:离职后不可以点击排班
aabc85 对比 | 目录
2026-07-07 huminmin
转正申请,调岗申请,工作交接申请,申请人可以根据配置得选项来源,选择数据
a29cb2 对比 | 目录
2026-07-07 huminmin
班次配置页面加上部门筛选、班次筛选框
35fc51 对比 | 目录
2026-07-06 zhangwencui
排班模块逻辑修改
59e490 对比 | 目录
已修改16个文件
已删除1个文件
683 ■■■■ 文件已修改
deploy.bat 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/YTJZLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/config.json 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
multiple/multiple-build.js 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/login.vue 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-list/components/FormPayloadFields.vue 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-template/selectOptionSource.js 77 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-template/useSelectOptionSources.js 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/regular-apply/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/transfer-apply/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/work-handover/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue 151 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/attendanceCheckin/checkinRules/index.vue 93 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/classsSheduling/index.vue 270 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
deploy.bat
@@ -3,12 +3,12 @@
setlocal enabledelayedexpansion
:: ================= Config =================
set "USER=dell"
set "PASS=xd123456@.."
set "IP=1.15.17.182"
set "PORT=10000"
set "USER=root"
set "PASS=Xdrjkj@10086#"
set "IP=36.213.180.217
set "PORT=65022"
set "DIST_DIR=dist"
set "REMOTE_DIR=/home/vue/newpro-product"
set "REMOTE_DIR=/data/project/pim/vue/xjgm"
set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10"
:: ===========================================
multiple/assets/logo/YTJZLogo.png
Binary files differ
multiple/config.json
@@ -123,11 +123,11 @@
    "logo": "logo/LSYLogo.png",
    "favicon": "favicon/LSYfavicon.ico"
  },
  "HTHB": {
  "HTHB-TEST": {
    "env": {
      "VITE_APP_TITLE": "山西宏腾新型环保科技有限责任公司",
      "VITE_BASE_API": "http://36.133.45.183:9090",
      "VITE_JAVA_API": "http://36.133.45.183:9089"
      "VITE_BASE_API": "http://1.15.17.182:9089",
      "VITE_JAVA_API": "http://1.15.17.182:9090"
    },
    "logo": "logo/HTHBLogo.png",
    "favicon": "favicon/HTHBfavicon.ico"
@@ -258,6 +258,15 @@
    "logo": "logo/JXSMLogo.png",
    "favicon": "favicon/JXSMico.ico"
  },
  "HYZC": {
    "env": {
      "VITE_APP_TITLE": "山西华亿众成建材有限公司",
      "VITE_BASE_API": "http://36.213.180.217:9009",
      "VITE_JAVA_API": "http://36.213.180.217:9008"
    },
    "logo": "logo/HYZCLogo.png",
    "favicon": "favicon/HYZCfavicon.ico"
  },
  "logo": "/src/assets/logo/logo.png",
  "favicon": "/public/favicon.ico"
}
multiple/multiple-build.js
@@ -60,7 +60,9 @@
    const backupFile = path.join(replacePath, config[key]);
    const replaceFile = path.join(resourcePath, companyMap[key]);
    await copyFileWithOverwrite(originFile, backupFile);
    if (fsSync.existsSync(originFile)) {
      await copyFileWithOverwrite(originFile, backupFile);
    }
    await copyFileWithOverwrite(replaceFile, originFile);
  }
@@ -82,7 +84,9 @@
      const originFile = path.join(rootPath, config[key]);
      const backupFile = path.join(replacePath, config[key]);
      await copyFileWithOverwrite(backupFile, originFile);
      if (fsSync.existsSync(backupFile)) {
        await copyFileWithOverwrite(backupFile, originFile);
      }
    }
    await fs.rm(replacePath, { recursive: true, force: true });
    console.log(`🗑️ 已删除 ${replacePath}`);
src/views/login.vue
@@ -2,16 +2,16 @@
  <div class="login-page">
    <main class="page">
      <section class="factory">
        <div class="brand hero-brand">
          <div class="logo hero-logo">
            <img
              :src="brandLogoUrl"
              :alt="`${companyName} logo`"
              class="logo-image hero-logo-image"
              @error="handleLogoError"
            />
          </div>
        </div>
<!--        <div class="brand hero-brand">-->
<!--          <div class="logo hero-logo">-->
<!--            <img-->
<!--              :src="brandLogoUrl"-->
<!--              :alt="`${companyName} logo`"-->
<!--              class="logo-image hero-logo-image"-->
<!--              @error="handleLogoError"-->
<!--            />-->
<!--          </div>-->
<!--        </div>-->
        <div class="hero">
          <div class="chip">数字工厂 · 智能排产 · 设备互联 · 质量追溯</div>
src/views/officeProcessAutomation/ApproveManage/approve-list/components/FormPayloadFields.vue
@@ -80,12 +80,12 @@
          clearable
          filterable
        >
          <el-option
            v-for="o in getOptions(field)"
            :key="String(o.value)"
            :label="o.label"
            :value="o.value"
          />
        <el-option
          v-for="o in getOptions(field, { moduleKey: props.moduleKey })"
          :key="String(o.value)"
          :label="o.label"
          :value="o.value"
        />
        </el-select>
        <span v-else class="field-value">{{ displayValue(field) }}</span>
      </el-form-item>
@@ -103,13 +103,14 @@
  fields: { type: Array, default: () => [] },
  formPayload: { type: Object, default: () => ({}) },
  readonly: { type: Boolean, default: false },
  moduleKey: { type: String, default: "" },
});
const { loading: optionSourceLoading, ensureForFields, getOptions, getDisplayLabel } =
  useSelectOptionSources();
async function loadOptionCaches() {
  await ensureForFields(props.fields);
  await ensureForFields(props.fields, { moduleKey: props.moduleKey });
}
onMounted(() => {
@@ -127,7 +128,7 @@
function displayValue(field) {
  const val = props.formPayload?.[field.key];
  if (field.type === "select" && field.optionSource && field.optionSource !== "static") {
    return getDisplayLabel(field, val);
    return getDisplayLabel(field, val, { moduleKey: props.moduleKey });
  }
  return formatFieldDisplayValue(field, val);
}
src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue
@@ -24,6 +24,7 @@
        v-model:attachments="form.storageBlobDTOs"
        :template-attachments="form.templateAttachments"
        :user-options="userOptions"
        :module-key="moduleKey"
        :show-template-name="!isEdit"
        :allow-change-template="false"
        :flow-attachments-only="flowAttachmentsOnly"
@@ -56,6 +57,7 @@
  fields: { type: Array, default: () => [] },
  activeTemplate: { type: Object, default: null },
  userOptions: { type: Array, default: () => [] },
  moduleKey: { type: String, default: "" },
  isEdit: { type: Boolean, default: false },
  saving: { type: Boolean, default: false },
  formRef: { type: Object, default: null },
src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue
@@ -4,7 +4,7 @@
  用法:
  <ApprovalTemplateBindDialog
    v-model:visible="visible"
    module-key="regular"
    :module-key="APPROVAL_MODULE_KEYS.REGULAR"
    @confirm="onTemplateBound"
  />
-->
src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue
@@ -15,6 +15,7 @@
      v-if="!hideFormFields && !flowAttachmentsOnly && !flowOnly"
      :fields="fields"
      :form-payload="formPayload"
      :module-key="moduleKey"
    />
    <el-form-item label="审批流程" required>
@@ -87,6 +88,7 @@
  uploadLimit: { type: Number, default: 10 },
  /** 为 true 时可编辑模板预置的审批人(仅审批模板管理页使用) */
  flowEditable: { type: Boolean, default: false },
  moduleKey: { type: String, default: "" },
});
const emit = defineEmits(["update:flowNodes", "update:attachments", "change-template"]);
src/views/officeProcessAutomation/ApproveManage/approve-template/selectOptionSource.js
@@ -1,30 +1,42 @@
import { deptTreeSelect, userListNoPageByTenantId } from "@/api/system/user.js";
import { staffOnJobListPage } from "@/api/personnelManagement/staffOnJob.js";
/** 下拉选项来源(写入 formConfig,提交页按来源拉取数据) */
export const SELECT_OPTION_SOURCE = {
  STATIC: "static",
  USER: "user",
  DEPT: "dept",
  EMPLOYEE: "employee",
};
export const SELECT_OPTION_SOURCE_OPTIONS = [
  { value: SELECT_OPTION_SOURCE.STATIC, label: "手动配置", desc: "在模板中自定义选项文本与值" },
  { value: SELECT_OPTION_SOURCE.USER, label: "人员列表", desc: "从系统用户中选择,值为用户 ID" },
  { value: SELECT_OPTION_SOURCE.DEPT, label: "部门列表", desc: "从组织架构中选择,值为部门 ID" },
  { value: SELECT_OPTION_SOURCE.EMPLOYEE, label: "员工列表", desc: "从在职员工中选择,值为员工 ID" },
];
const EMPLOYEE_APPLICANT_MODULE_KEYS = new Set(["regular"]);
export function selectOptionSourceLabel(source) {
  return SELECT_OPTION_SOURCE_OPTIONS.find((x) => x.value === source)?.label || "—";
}
export function isDynamicOptionSource(source) {
  return source === SELECT_OPTION_SOURCE.USER || source === SELECT_OPTION_SOURCE.DEPT;
  return (
    source === SELECT_OPTION_SOURCE.USER ||
    source === SELECT_OPTION_SOURCE.DEPT ||
    source === SELECT_OPTION_SOURCE.EMPLOYEE
  );
}
function unwrapArray(payload) {
  if (Array.isArray(payload)) return payload;
  if (Array.isArray(payload?.records)) return payload.records;
  if (payload?.data && Array.isArray(payload.data)) return payload.data;
  if (Array.isArray(payload?.data?.records)) return payload.data.records;
  if (payload?.rows && Array.isArray(payload.rows)) return payload.rows;
  if (Array.isArray(payload?.data?.rows)) return payload.data.rows;
  return [];
}
@@ -39,6 +51,15 @@
  const value = u.userId ?? u.id;
  return {
    label: u.nickName || u.userName || `用户${value}`,
    value,
  };
}
/** 在职员工 → 下拉 option */
export function mapEmployeeToSelectOption(u) {
  const value = u.id ?? u.staffOnJobId ?? u.staffId;
  return {
    label: u.staffName || u.name || u.userName || `员工${value}`,
    value,
  };
}
@@ -70,21 +91,41 @@
}
/** 按字段配置解析下拉 options(需传入已加载的缓存) */
export function resolveFieldSelectOptions(field, caches = {}) {
function isApplicantField(field) {
  const key = String(field?.key || "").toLowerCase();
  const label = String(field?.label || "");
  return (
    key === "applicant" ||
    key === "applicantname" ||
    label.includes("申请人")
  );
}
export function resolveFieldSelectOptions(field, caches = {}, context = {}) {
  const source = field?.optionSource || SELECT_OPTION_SOURCE.STATIC;
  if (
    source === SELECT_OPTION_SOURCE.USER &&
    EMPLOYEE_APPLICANT_MODULE_KEYS.has(context.moduleKey) &&
    isApplicantField(field)
  ) {
    return (caches.employees || []).map(mapEmployeeToSelectOption);
  }
  if (source === SELECT_OPTION_SOURCE.USER) {
    return (caches.users || []).map(mapUserToSelectOption);
  }
  if (source === SELECT_OPTION_SOURCE.DEPT) {
    return caches.deptOptions || [];
  }
  if (source === SELECT_OPTION_SOURCE.EMPLOYEE) {
    return (caches.employees || []).map(mapEmployeeToSelectOption);
  }
  return (field?.options || []).filter((o) => o.value !== "" && o.value != null);
}
/** 根据已解析的 options 反查展示文本 */
export function resolveSelectDisplayLabel(field, val, caches = {}) {
export function resolveSelectDisplayLabel(field, val, caches = {}, context = {}) {
  if (val == null || val === "") return "—";
  const options = resolveFieldSelectOptions(field, caches);
  const options = resolveFieldSelectOptions(field, caches, context);
  const hit = options.find((o) => String(o.value) === String(val));
  return hit?.label || String(val);
}
@@ -93,9 +134,10 @@
export async function fetchSelectOptionCaches(sources = []) {
  const needUser = sources.includes(SELECT_OPTION_SOURCE.USER);
  const needDept = sources.includes(SELECT_OPTION_SOURCE.DEPT);
  const caches = { users: [], deptOptions: [] };
  const needEmployee = sources.includes(SELECT_OPTION_SOURCE.EMPLOYEE);
  const caches = { users: [], deptOptions: [], employees: [] };
  if (!needUser && !needDept) return caches;
  if (!needUser && !needDept && !needEmployee) return caches;
  const tasks = [];
  if (needUser) {
@@ -123,17 +165,36 @@
        })
    );
  }
  if (needEmployee) {
    tasks.push(
      staffOnJobListPage({ current: -1, size: -1, staffState: 1 })
        .then((res) => {
          caches.employees = unwrapArray(res);
        })
        .catch(() => {
          caches.employees = [];
        })
    );
  }
  await Promise.all(tasks);
  return caches;
}
/** 从字段列表收集需要预加载的动态来源 */
export function collectOptionSourcesFromFields(fields) {
export function collectOptionSourcesFromFields(fields, context = {}) {
  const set = new Set();
  (fields || []).forEach((f) => {
    if (f?.type === "select" && isDynamicOptionSource(f.optionSource)) {
      set.add(f.optionSource);
      if (
        EMPLOYEE_APPLICANT_MODULE_KEYS.has(context.moduleKey) &&
        f.optionSource === SELECT_OPTION_SOURCE.USER &&
        isApplicantField(f)
      ) {
        set.add(SELECT_OPTION_SOURCE.EMPLOYEE);
      } else {
        set.add(f.optionSource);
      }
    }
  });
  return [...set];
src/views/officeProcessAutomation/ApproveManage/approve-template/useSelectOptionSources.js
@@ -14,25 +14,26 @@
    deptOptions: [],
  });
  async function ensureForFields(fields) {
    const sources = collectOptionSourcesFromFields(fields);
  async function ensureForFields(fields, context = {}) {
    const sources = collectOptionSourcesFromFields(fields, context);
    if (!sources.length) return;
    loading.value = true;
    try {
      const next = await fetchSelectOptionCaches(sources);
      caches.users = next.users;
      caches.deptOptions = next.deptOptions;
      caches.employees = next.employees || [];
    } finally {
      loading.value = false;
    }
  }
  function getOptions(field) {
    return resolveFieldSelectOptions(field, caches);
  function getOptions(field, context = {}) {
    return resolveFieldSelectOptions(field, caches, context);
  }
  function getDisplayLabel(field, val) {
    return resolveSelectDisplayLabel(field, val, caches);
  function getDisplayLabel(field, val, context = {}) {
    return resolveSelectDisplayLabel(field, val, caches, context);
  }
  return {
src/views/officeProcessAutomation/HrManage/regular-apply/index.vue
@@ -48,6 +48,7 @@
      :fields="submitFormFields"
      :active-template="activeTemplate"
      :user-options="flowUserOptions"
      :module-key="APPROVAL_MODULE_KEYS.REGULAR"
      :is-edit="isSubmitEdit"
      :saving="submitSaving"
      :form-ref="submitFormRef"
src/views/officeProcessAutomation/HrManage/transfer-apply/index.vue
@@ -58,6 +58,7 @@
      :fields="submitFormFields"
      :active-template="activeTemplate"
      :user-options="flowUserOptions"
      :module-key="APPROVAL_MODULE_KEYS.TRANSFER"
      :is-edit="isSubmitEdit"
      :saving="submitSaving"
      :form-ref="submitFormRef"
src/views/officeProcessAutomation/HrManage/work-handover/index.vue
@@ -58,6 +58,7 @@
      :fields="submitFormFields"
      :active-template="activeTemplate"
      :user-options="flowUserOptions"
      :module-key="APPROVAL_MODULE_KEYS.WORK_HANDOVER"
      :is-edit="isSubmitEdit"
      :saving="submitSaving"
      :form-ref="submitFormRef"
src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue
@@ -37,11 +37,17 @@
          <el-option v-for="item in shifts_list"
                     :key="item.value"
                     :label="item.label"
                     :value="item.value" />
                     :value="item.value"
                     :disabled="
              operationType === 'add' &&
              (String(item.label || '').includes('假') ||
                String(item.label || '').includes('休'))
            " />
        </el-select>
      </el-form-item>
      <el-form-item label="打卡范围(m)"
                    prop="radius">
                    prop="radius"
                    v-show="!hideClockFields">
        <el-input-number v-model="form.radius"
                         :min="10"
                         :max="1000"
@@ -51,7 +57,8 @@
      </el-form-item>
      <!-- 高德地图选择 -->
      <el-form-item label="打卡位置"
                    prop="longitude">
                    prop="longitude"
                    v-show="!hideClockFields">
        <div class="map-container">
          <div class="map-header"
               style="margin-bottom: 10px">
@@ -82,14 +89,16 @@
        </div>
      </el-form-item>
      <el-form-item label="地点名称"
                    prop="locationName">
                    prop="locationName"
                    v-show="!hideClockFields">
        <el-input v-model="form.locationName"
                  :disabled="operationType === 'view'"
                  placeholder="请输入地点名称" />
      </el-form-item>
      <!-- 上下班时间 -->
      <el-form-item label="上班时间"
                    prop="startAt">
                    prop="startAt"
                    v-show="!hideClockFields">
        <el-time-picker v-model="form.startAt"
                        format="HH:mm"
                        value-format="HH:mm"
@@ -97,7 +106,8 @@
                        :disabled="operationType === 'view'" />
      </el-form-item>
      <el-form-item label="下班时间"
                    prop="endAt">
                    prop="endAt"
                    v-show="!hideClockFields">
        <el-time-picker v-model="form.endAt"
                        format="HH:mm"
                        value-format="HH:mm"
@@ -110,7 +120,9 @@
    </el-form>
    <template #footer>
      <span class="dialog-footer">
        <el-button type="primary" @click="submitForm" v-if="operationType !== 'view'">确定</el-button>
        <el-button type="primary"
                   @click="submitForm"
                   v-if="operationType !== 'view'">确定</el-button>
        <el-button @click="dialogVisible = false">取消</el-button>
      </span>
    </template>
@@ -157,6 +169,21 @@
  // 获取班次字典值
  const { shifts_list } = useDict("shifts_list");
  const selectedShiftLabel = computed(() => {
    const item = shifts_list.value?.find(
      s => String(s.value) === String(form.shift)
    );
    return item?.label || "";
  });
  const hideClockFields = computed(() => {
    if (props.operationType === "view") return false;
    return (
      selectedShiftLabel.value.includes("假") ||
      selectedShiftLabel.value.includes("休")
    );
  });
  // 表单数据
  const formRef = ref();
  const form = reactive({
@@ -172,44 +199,53 @@
  });
  // 表单验证规则
  const rules = {
    sysDeptId: [{ required: true, message: "请选择部门", trigger: "change" }],
    locationName: [
      { required: true, message: "请输入地点名称", trigger: "blur" },
    ],
    longitude: [{ required: true, message: "请选择打卡位置", trigger: "blur" }],
    latitude: [{ required: true, message: "请选择打卡位置", trigger: "blur" }],
    shift: [{ required: true, message: "请选择班次", trigger: "change" }],
    radius: [{ required: true, message: "请输入打卡范围", trigger: "blur" }],
    startAt: [{ required: true, message: "请选择上班时间", trigger: "change" }],
    endAt: [
      { required: true, message: "请选择下班时间", trigger: "change" },
      {
        validator: (rule, value, callback) => {
          // 如果是夜班,不限制时间顺序
          if (form.shift && ['夜班', '夜'].includes(form.shift)) {
            callback();
            return;
          }
          if (form.startAt && value) {
            const startParts = form.startAt.split(":");
            const endParts = value.split(":");
            const startTime =
              parseInt(startParts[0]) * 60 + parseInt(startParts[1]);
            const endTime = parseInt(endParts[0]) * 60 + parseInt(endParts[1]);
            if (endTime <= startTime) {
              callback(new Error("下班时间不能早于上班时间"));
  const rules = computed(() => {
    const base = {
      sysDeptId: [{ required: true, message: "请选择部门", trigger: "change" }],
      shift: [{ required: true, message: "请选择班次", trigger: "change" }],
    };
    if (hideClockFields.value) {
      return base;
    }
    return {
      ...base,
      locationName: [
        { required: true, message: "请输入地点名称", trigger: "blur" },
      ],
      longitude: [{ required: true, message: "请选择打卡位置", trigger: "blur" }],
      latitude: [{ required: true, message: "请选择打卡位置", trigger: "blur" }],
      radius: [{ required: true, message: "请输入打卡范围", trigger: "blur" }],
      startAt: [{ required: true, message: "请选择上班时间", trigger: "change" }],
      endAt: [
        { required: true, message: "请选择下班时间", trigger: "change" },
        {
          validator: (rule, value, callback) => {
            if (form.shift && ["夜班", "夜"].includes(form.shift)) {
              callback();
              return;
            }
            if (form.startAt && value) {
              const startParts = form.startAt.split(":");
              const endParts = value.split(":");
              const startTime =
                parseInt(startParts[0]) * 60 + parseInt(startParts[1]);
              const endTime = parseInt(endParts[0]) * 60 + parseInt(endParts[1]);
              if (endTime <= startTime) {
                callback(new Error("下班时间不能早于上班时间"));
              } else {
                callback();
              }
            } else {
              callback();
            }
          } else {
            callback();
          }
          },
          trigger: "change",
        },
        trigger: "change",
      },
    ],
  };
      ],
    };
  });
  // 部门选项
  const deptOptions = ref([]);
@@ -393,8 +429,8 @@
  watch(
    () => form.radius,
    newValue => {
      if (circle) {
        circle.setRadius(newValue);
      if (circle && newValue != null && newValue !== "") {
        circle.setRadius(Number(newValue));
      }
    }
  );
@@ -405,6 +441,29 @@
    () => {
      if (formRef.value && form.endAt) {
        formRef.value.validateField("endAt");
      }
    }
  );
  watch(
    () => hideClockFields.value,
    val => {
      if (!val) return;
      form.radius = null;
      form.longitude = "";
      form.latitude = "";
      form.locationName = "";
      form.startAt = "";
      form.endAt = "";
      if (formRef.value) {
        formRef.value.clearValidate([
          "radius",
          "longitude",
          "latitude",
          "locationName",
          "startAt",
          "endAt",
        ]);
      }
    }
  );
@@ -442,7 +501,9 @@
        // 初始化地图
        setTimeout(() => {
          initMap();
          if (!hideClockFields.value) {
            initMap();
          }
        }, 100);
      }
    }
@@ -513,4 +574,4 @@
  .mt8 {
    margin-top: 8px;
  }
</style>
</style>
src/views/personnelManagement/attendanceCheckin/checkinRules/index.vue
@@ -13,30 +13,37 @@
        </el-button>
      </div>
    </div>
    <!-- 查询条件 -->
    <!-- <el-form :model="searchForm"
    <el-form :model="searchForm"
             :inline="true"
             class="search-form mb16">
      <el-form-item label="部门:"
                    prop="countId">
        <el-tree-select v-model="searchForm.countId"
                    prop="sysDeptId">
        <el-tree-select v-model="searchForm.sysDeptId"
                        :data="deptOptions"
                        :props="{ value: 'id', label: 'label', children: 'children' }"
                        value-key="id"
                        placeholder="请选择部门"
                        check-strictly
                        clearable
                        style="width: 200px" />
      </el-form-item>
      <el-form-item label="地点:"
                    prop="locationName">
        <el-input v-model="searchForm.locationName"
                  placeholder="请输入地点名称"
                  clearable
                  style="width: 200px" />
      <el-form-item label="班次:"
                    prop="shift">
        <el-select v-model="searchForm.shift"
                   placeholder="请选择班次"
                   clearable
                   style="width: 200px">
          <el-option
              v-for="item in shifts_list"
              :key="item.value"
              :label="item.label"
              :value="item.value"
          />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary"
                   @click="fetchData">
                   @click="handleSearch">
          <el-icon>
            <Search />
          </el-icon>
@@ -49,7 +56,7 @@
          重置
        </el-button>
      </el-form-item>
    </el-form> -->
    </el-form>
    <!-- 班次列表 -->
    <el-card shadow="never"
             class="mb16">
@@ -74,23 +81,39 @@
          </template>
        </el-table-column>
        <el-table-column prop="locationName"
                         label="地点名称" />
                         label="地点名称">
          <template #default="scope">
            {{ scope.row.locationName || "-" }}
          </template>
        </el-table-column>
        <el-table-column prop="longitude"
                         label="经度" />
                         label="经度">
          <template #default="scope">
            {{ scope.row.longitude ?? "-" }}
          </template>
        </el-table-column>
        <el-table-column prop="latitude"
                         label="纬度" />
                         label="纬度">
          <template #default="scope">
            {{ scope.row.latitude ?? "-" }}
          </template>
        </el-table-column>
        <el-table-column prop="radius"
                         label="打卡范围(m)" />
                         label="打卡范围(m)">
          <template #default="scope">
            {{ scope.row.radius ?? "-" }}
          </template>
        </el-table-column>
        <el-table-column prop="startAt"
                         label="上班时间">
          <template #default="scope">
            {{ scope.row.startAt }}
            {{ scope.row.startAt || "-" }}
          </template>
        </el-table-column>
        <el-table-column prop="endAt"
                         label="下班时间">
          <template #default="scope">
            {{ scope.row.endAt }}
            {{ scope.row.endAt || "-" }}
          </template>
        </el-table-column>
        <el-table-column label="操作"
@@ -100,9 +123,11 @@
          <template #default="scope">
            <el-button type="primary"
                       link
                       :disabled="isRuleLocked(scope.row)"
                       @click="openForm('edit', scope.row)">编辑</el-button>
            <el-button type="danger"
                       link
                       :disabled="isRuleLocked(scope.row)"
                       @click="handleDelete(scope.row.id)">删除</el-button>
          </template>
        </el-table-column>
@@ -124,7 +149,7 @@
</template>
<script setup>
  import { ref, reactive, onMounted } from "vue";
  import { ref, reactive, onMounted, getCurrentInstance } from "vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import {
    Plus,
@@ -158,8 +183,8 @@
  // 查询表单
  const searchForm = reactive({
    countId: "",
    locationName: "",
    sysDeptId: "",
    shift: "",
  });
  // 部门选项
@@ -187,6 +212,10 @@
    if (!value) return "";
    const shift = shifts_list.value.find(item => item.value === value);
    return shift ? shift.label : value;
  };
  const isRuleLocked = row => {
    return Boolean(row?.usageLocked || row?.canEdit === false || row?.canDelete === false);
  };
  // 获取部门列表
@@ -232,12 +261,18 @@
    tableLoading.value = true;
    getAttendanceRules({ ...page, ...searchForm })
      .then(res => {
        tableData.value = res.data.records;
        const records = Array.isArray(res?.data?.records) ? res.data.records : [];
        tableData.value = records.filter(r => String(r?.id) !== "999");
        page.total = res.data.total;
      })
      .finally(() => {
        tableLoading.value = false;
      });
  };
  const handleSearch = () => {
    page.current = 1;
    fetchData();
  };
  // 分页变更
@@ -249,13 +284,18 @@
  // 重置搜索
  const resetSearch = () => {
    searchForm.countId = "";
    searchForm.locationName = "";
    searchForm.sysDeptId = "";
    searchForm.shift = "";
    page.current = 1;
    fetchData();
  };
  // 打开表单
  const openForm = (type, row = {}) => {
    if (type === "edit" && isRuleLocked(row)) {
      ElMessage.warning("该班次配置已被排班并产生打卡记录,不能编辑");
      return;
    }
    operationType.value = type;
    currentRow.value = row;
    dialogVisible.value = true;
@@ -263,6 +303,11 @@
  // 删除班次
  const handleDelete = id => {
    const row = tableData.value.find(item => String(item.id) === String(id));
    if (isRuleLocked(row)) {
      ElMessage.warning("存在已排班且已有打卡记录的班次配置,不能删除");
      return;
    }
    ElMessageBox.confirm("确定要删除这条班次吗?", "删除确认", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
src/views/personnelManagement/classsSheduling/index.vue
@@ -142,19 +142,20 @@
                     :key="'d' + i">
                  <el-dropdown trigger="click"
                               placement="bottom"
                               @command="(e) => handleCommand(e, m)"
                               :disabled="isCellBlockedByLeaveDate(item, m)"
                               @command="(e) => handleCommand(e, item, m)"
                               class="shift-dropdown">
                    <div class="shift-box"
                         :class="{
                      'shift-box-early': m.shift === '早班',
                      'shift-box-mid': m.shift === '中班',
                      'shift-box-night': m.shift === '夜班',
                      'shift-box-rest': m.shift === '休息',
                      'shift-box-rest': !m.shift || m.shift === '休息',
                      'shift-box-leave': m.shift === '请假',
                      'shift-box-other': m.shift === '夜11',
                      'shift-box-business': m.shift === '夜12',
                    }">
                      <span class="shift-text">{{ getShiftNameByValue(m.shift) || '—' }}</span>
                      <span class="shift-text">{{ getShiftNameByValue(m.shift) || '休息' }}</span>
                    </div>
                    <template #dropdown>
                      <el-dropdown-menu>
@@ -199,7 +200,7 @@
              </div> -->
              <div class="user-total">
                <span class="total-label">合计出勤:</span>
                <span class="total-value">{{ item.work_time }}天</span>
                <span class="total-value">{{ item.year.totalYearAttendance }}天</span>
              </div>
            </div>
          </div>
@@ -271,6 +272,23 @@
      <div class="search_thing">
        <div class="search_label"
             style="width: 90px">
          <span style="color: red; margin-right: 4px">*</span>部门:
        </div>
        <div class="search_input">
          <el-tree-select v-model="schedulingQuery.deptId"
                          :data="deptOptions"
                          :props="{ value: 'id', label: 'label', children: 'children' }"
                          value-key="id"
                          placeholder="请选择部门"
                          check-strictly
                          clearable
                          @change="onSchedulingDeptChange"
                          style="width: 100%" />
        </div>
      </div>
      <div class="search_thing">
        <div class="search_label"
             style="width: 90px">
          <span style="color: red; margin-right: 4px">*</span>人员名称:
        </div>
        <div class="search_input">
@@ -279,7 +297,8 @@
                     style="width: 100%"
                     multiple
                     clearable
                     collapse-tags>
                     collapse-tags
                     :disabled="!schedulingQuery.deptId">
            <el-option v-for="item in personList"
                       :key="item.id"
                       :label="item.staffName"
@@ -296,8 +315,9 @@
        <div class="search_input">
          <el-select v-model="schedulingQuery.shift"
                     placeholder="请选择"
                     style="width: 100%">
            <el-option v-for="item in classType"
                     style="width: 100%"
                     :disabled="!schedulingQuery.deptId">
            <el-option v-for="item in schedulingClassType"
                       :key="item.id"
                       :label="getShiftNameByValue(item.shift)"
                       :value="item.id">
@@ -318,7 +338,14 @@
</template>
<script setup>
  import { ref, reactive, onMounted, getCurrentInstance } from "vue";
  import {
    ref,
    reactive,
    computed,
    watch,
    onMounted,
    getCurrentInstance,
  } from "vue";
  import { useRouter } from "vue-router";
  import {
    page,
@@ -328,6 +355,7 @@
    update,
    staffOnJobListPage,
  } from "@/api/personnelManagement/class";
  import { findStaffLeaveListPage } from "@/api/personnelManagement/staffLeave.js";
  import { deptTreeSelect } from "@/api/system/user.js";
  import { getAttendanceRules } from "@/api/personnelManagement/attendanceRules.js";
  import { useDict } from "@/utils/dict";
@@ -376,6 +404,8 @@
  // 人员列表
  const personList = ref([]);
  const personListAll = ref([]);
  const leaveDateMap = ref({});
  // 加载状态
  const loading = ref(false);
@@ -383,8 +413,18 @@
  // 排班查询条件
  const schedulingQuery = reactive({
    week: "",
    deptId: "",
    userId: null,
    shift: "",
  });
  const schedulingClassType = computed(() => {
    if (!schedulingQuery.deptId) return classType.value || [];
    return (classType.value || []).filter(item => {
      if (["998", "999"].includes(String(item?.id))) return true;
      const itemDeptId = item.deptId ?? item.sysDeptId ?? item.dept_id;
      return String(itemDeptId) === String(schedulingQuery.deptId);
    });
  });
  // 列表数据
@@ -593,6 +633,10 @@
      proxy.$modal.msgError("请选择周次");
      return;
    }
    if (!schedulingQuery.deptId) {
      proxy.$modal.msgError("请选择部门");
      return;
    }
    let time = schedulingQuery.week.getTime();
    // 格式化日期为 YYYY-MM-DD 格式
@@ -616,6 +660,15 @@
      proxy.$modal.msgError("请选择班次");
      return;
    }
    const blockedEmployees = (personList.value || []).filter(item => {
      const leaveDate = getEmployeeLeaveDate(item?.id);
      const weekEndDate = getSchedulingWeekEndDate();
      return leaveDate && weekEndDate && weekEndDate.getTime() > leaveDate.getTime();
    });
    if (blockedEmployees.length) {
      proxy.$modal.msgError("存在已超过离职日期的人员,不能继续排班");
      return;
    }
    loading.value = true;
    add({
      startWeek,
@@ -628,8 +681,10 @@
        proxy.$modal.msgSuccess("操作成功");
        schedulingVisible.value = false;
        schedulingQuery.week = "";
        schedulingQuery.deptId = "";
        schedulingQuery.userId = null;
        schedulingQuery.shift = "";
        personList.value = [];
        refresh();
      })
      .catch(err => {
@@ -683,17 +738,20 @@
        }
        proxy.$download.saveAs(blob, fileName + ".xlsx");
      })
      .catch(err => {
      })
      .catch(err => {})
      .finally(() => {
        downLoading.value = false;
      });
  };
  // 处理命令
  const handleCommand = (e, m) => {
  const handleCommand = (e, row, cell) => {
    if (isCellBlockedByLeaveDate(row, cell)) {
      proxy.$modal.msgError("该员工已超出离职日期,不能继续排班");
      return;
    }
    // if (e != m.shift) {
    update({
      id: m.id,
      id: cell.id,
      personalAttendanceLocationConfigId: e,
    }).then(res => {
      proxy.$modal.msgSuccess("操作成功");
@@ -709,7 +767,18 @@
  // 查询规则列表
  const fetchData = () => {
    getAttendanceRules({ current: -1, size: -1 }).then(res => {
      classType.value = res.data.records;
      const records = Array.isArray(res?.data?.records) ? res.data.records : [];
      const appendList = [
        { id: 998, shift: "休息" },
        { id: 999, shift: "请假" },
      ];
      const exists = new Set(records.map(r => String(r?.id)));
      appendList.forEach(item => {
        if (!exists.has(String(item.id))) {
          records.push(item);
        }
      });
      classType.value = records;
    });
  };
  // 获取用户
@@ -724,8 +793,139 @@
      staffState: 1,
    }).then(res => {
      let arr = res.data.records;
      personList.value = arr;
      personListAll.value = arr;
      updatePersonListByDept();
    });
  };
  const getLeaveDates = () => {
    findStaffLeaveListPage({
      current: -1,
      size: -1,
    }).then(res => {
      const records = Array.isArray(res?.data?.records) ? res.data.records : [];
      const map = {};
      records.forEach(item => {
        const staffId = item?.staffOnJobId ?? item?.staff_on_job_id;
        const leaveDate = item?.leaveDate ?? item?.leave_date;
        if (!staffId || !leaveDate) return;
        const key = String(staffId);
        const prev = map[key];
        if (!prev || String(leaveDate) > String(prev)) {
          map[key] = leaveDate;
        }
      });
      leaveDateMap.value = map;
      updatePersonListByDept();
      syncPersonListSelection();
    });
  };
  const findDeptNodeById = (deptId, deptList = deptOptions.value) => {
    const list = Array.isArray(deptList) ? deptList : [];
    for (const node of list) {
      if (String(node.id) === String(deptId)) return node;
      if (node.children && node.children.length) {
        const found = findDeptNodeById(deptId, node.children);
        if (found) return found;
      }
    }
    return null;
  };
  const collectDeptIds = node => {
    const ids = [];
    if (!node) return ids;
    ids.push(String(node.id));
    if (node.children && node.children.length) {
      node.children.forEach(child => {
        ids.push(...collectDeptIds(child));
      });
    }
    return ids;
  };
  const updatePersonListByDept = () => {
    if (!schedulingQuery.deptId) {
      personList.value = [];
      return;
    }
    const node = findDeptNodeById(schedulingQuery.deptId);
    const deptIds = new Set(collectDeptIds(node));
    personList.value = (personListAll.value || []).filter(item => {
      const itemDeptId = item.deptId ?? item.sysDeptId ?? item.dept_id;
      return deptIds.has(String(itemDeptId)) && !isEmployeeBlockedByLeaveDate(item);
    });
  };
  const parseDateOnly = value => {
    if (!value) return null;
    if (value instanceof Date) {
      const d = new Date(value);
      d.setHours(0, 0, 0, 0);
      return d;
    }
    const normalized = String(value).trim().slice(0, 10);
    if (!normalized) return null;
    const d = new Date(normalized.replace(/-/g, "/"));
    if (Number.isNaN(d.getTime())) return null;
    d.setHours(0, 0, 0, 0);
    return d;
  };
  const getSchedulingWeekEndDate = () => {
    if (!schedulingQuery.week) return null;
    const start = new Date(schedulingQuery.week);
    start.setHours(0, 0, 0, 0);
    return new Date(start.getTime() + 24 * 60 * 60 * 1000 * 5);
  };
  const getCellDate = cell => parseDateOnly(cell?.time);
  const getEmployeeLeaveDate = staffId => {
    const leaveDate = leaveDateMap.value[String(staffId)];
    return parseDateOnly(leaveDate);
  };
  const isEmployeeBlockedByLeaveDate = item => {
    const leaveDate = getEmployeeLeaveDate(item?.id);
    if (!leaveDate) return false;
    const weekEndDate = getSchedulingWeekEndDate();
    if (!weekEndDate) return false;
    return weekEndDate.getTime() > leaveDate.getTime();
  };
  const isCellBlockedByLeaveDate = (row, cell) => {
    const staffId = row?.userId ?? row?.user_id ?? row?.id;
    const leaveDate = getEmployeeLeaveDate(staffId);
    const cellDate = getCellDate(cell);
    if (!leaveDate || !cellDate) return false;
    return cellDate.getTime() > leaveDate.getTime();
  };
  const syncPersonListSelection = () => {
    if (!Array.isArray(schedulingQuery.userId) || !schedulingQuery.userId.length) return;
    const validIds = new Set((personList.value || []).map(item => String(item.id)));
    const nextIds = schedulingQuery.userId.filter(id => validIds.has(String(id)));
    if (nextIds.length !== schedulingQuery.userId.length) {
      schedulingQuery.userId = nextIds;
      proxy.$modal.msgWarning("已自动剔除超出离职日期的人员");
    }
  };
  const setDefaultShiftByDept = () => {
    if (!schedulingQuery.deptId) {
      schedulingQuery.shift = "";
      return;
    }
    const first = schedulingClassType.value?.[0];
    schedulingQuery.shift = first ? first.id : "";
  };
  const onSchedulingDeptChange = () => {
    schedulingQuery.userId = null;
    updatePersonListByDept();
    setDefaultShiftByDept();
  };
  // 根据字典获取日期
@@ -749,6 +949,7 @@
  onMounted(() => {
    fetchData();
    getUsers();
    getLeaveDates();
    fetchDeptOptions();
    if (query.month) {
      init();
@@ -761,6 +962,45 @@
    }
    monthList.value.reverse();
  });
  watch(
    () => classType.value,
    () => {
      if (schedulingVisible.value && schedulingQuery.deptId) {
        setDefaultShiftByDept();
      }
    }
  );
  watch(
    () => schedulingQuery.week,
    () => {
      if (!schedulingVisible.value) return;
      updatePersonListByDept();
      syncPersonListSelection();
    }
  );
  watch(
    () => schedulingQuery.deptId,
    () => {
      if (!schedulingVisible.value) return;
      updatePersonListByDept();
      syncPersonListSelection();
    }
  );
  watch(
    () => schedulingVisible.value,
    val => {
      if (!val) return;
      schedulingQuery.week = "";
      schedulingQuery.deptId = "";
      schedulingQuery.userId = null;
      schedulingQuery.shift = "";
      personList.value = [];
    }
  );
</script>
<style scoped>
@@ -973,7 +1213,7 @@
  .user-stats {
    /* display: flex; */
    /* flex-wrap: wrap;
                                                                                                                                                                                                                                                                                        gap: 10px; */
                                                                                                                                                                                                                                                                                                      gap: 10px; */
    margin-bottom: 4px;
  }