Merge branch 'dev_NEW_pro' into dev_pro_山西长治
# Conflicts:
# multiple/config.json
| | |
| | | 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" |
| | | :: =========================================== |
| | | |
| | |
| | | "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" |
| | | }, |
| | | "OLD": { |
| | | "env": { |
| | | "VITE_APP_TITLE": "青海欧莱德新材料科技有限公司", |
| | | "VITE_BASE_API": "http://223.221.37.139:9000", |
| | | "VITE_JAVA_API": "http://223.221.37.139:9000" |
| | | }, |
| | | "logo": "logo/OLDLogo.png", |
| | | "favicon": "favicon/OLD.ico" |
| | | }, |
| | | "XJGM": { |
| | | "env": { |
| | | "VITE_APP_TITLE": "克拉玛依市新聚工贸有限责任公司", |
| | | "VITE_BASE_API": "http://36.213.180.217:9015", |
| | | "VITE_JAVA_API": "http://36.213.180.217:9016" |
| | | }, |
| | | "logo": "logo/XJGMLogo.png", |
| | | "favicon": "favicon/XJGM.ico" |
| | | }, |
| | | "RBHB": { |
| | | "env": { |
| | | "VITE_APP_TITLE": "克拉玛依融邦环保科技有限公司", |
| | | "VITE_BASE_API": "http://36.213.180.217:9017", |
| | | "VITE_JAVA_API": "http://36.213.180.217:9018" |
| | | }, |
| | | "logo": "logo/RBHBLogo.png", |
| | | "favicon": "favicon/RBHB.ico" |
| | | }, |
| | | "logo": "/src/assets/logo/logo.png", |
| | | "favicon": "/public/favicon.ico" |
| | | } |
| | |
| | | const backupFile = path.join(replacePath, config[key]); |
| | | const replaceFile = path.join(resourcePath, companyMap[key]); |
| | | |
| | | if (fsSync.existsSync(originFile)) { |
| | | await copyFileWithOverwrite(originFile, backupFile); |
| | | } |
| | | await copyFileWithOverwrite(replaceFile, originFile); |
| | | } |
| | | |
| | |
| | | |
| | | const originFile = path.join(rootPath, config[key]); |
| | | const backupFile = path.join(replacePath, config[key]); |
| | | if (fsSync.existsSync(backupFile)) { |
| | | await copyFileWithOverwrite(backupFile, originFile); |
| | | } |
| | | } |
| | | await fs.rm(replacePath, { recursive: true, force: true }); |
| | | console.log(`🗑️ 已删除 ${replacePath}`); |
| | | } |
| | |
| | | <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> |
| | |
| | | filterable |
| | | > |
| | | <el-option |
| | | v-for="o in getOptions(field)" |
| | | v-for="o in getOptions(field, { moduleKey: props.moduleKey })" |
| | | :key="String(o.value)" |
| | | :label="o.label" |
| | | :value="o.value" |
| | |
| | | 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(() => { |
| | |
| | | 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); |
| | | } |
| | |
| | | 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" |
| | |
| | | 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 }, |
| | |
| | | 用法: |
| | | <ApprovalTemplateBindDialog |
| | | v-model:visible="visible" |
| | | module-key="regular" |
| | | :module-key="APPROVAL_MODULE_KEYS.REGULAR" |
| | | @confirm="onTemplateBound" |
| | | /> |
| | | --> |
| | |
| | | v-if="!hideFormFields && !flowAttachmentsOnly && !flowOnly" |
| | | :fields="fields" |
| | | :form-payload="formPayload" |
| | | :module-key="moduleKey" |
| | | /> |
| | | |
| | | <el-form-item label="审批流程" required> |
| | |
| | | uploadLimit: { type: Number, default: 10 }, |
| | | /** 为 true 时可编辑模板预置的审批人(仅审批模板管理页使用) */ |
| | | flowEditable: { type: Boolean, default: false }, |
| | | moduleKey: { type: String, default: "" }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:flowNodes", "update:attachments", "change-template"]); |
| | |
| | | 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 []; |
| | | } |
| | | |
| | |
| | | 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, |
| | | }; |
| | | } |
| | |
| | | } |
| | | |
| | | /** 按字段配置解析下拉 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); |
| | | } |
| | |
| | | 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) { |
| | |
| | | }) |
| | | ); |
| | | } |
| | | 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)) { |
| | | 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]; |
| | | } |
| | |
| | | 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 { |
| | |
| | | :fields="submitFormFields" |
| | | :active-template="activeTemplate" |
| | | :user-options="flowUserOptions" |
| | | :module-key="APPROVAL_MODULE_KEYS.REGULAR" |
| | | :is-edit="isSubmitEdit" |
| | | :saving="submitSaving" |
| | | :form-ref="submitFormRef" |
| | |
| | | :fields="submitFormFields" |
| | | :active-template="activeTemplate" |
| | | :user-options="flowUserOptions" |
| | | :module-key="APPROVAL_MODULE_KEYS.TRANSFER" |
| | | :is-edit="isSubmitEdit" |
| | | :saving="submitSaving" |
| | | :form-ref="submitFormRef" |
| | |
| | | :fields="submitFormFields" |
| | | :active-template="activeTemplate" |
| | | :user-options="flowUserOptions" |
| | | :module-key="APPROVAL_MODULE_KEYS.WORK_HANDOVER" |
| | | :is-edit="isSubmitEdit" |
| | | :saving="submitSaving" |
| | | :form-ref="submitFormRef" |
| | |
| | | <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" |
| | |
| | | </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"> |
| | |
| | | </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" |
| | |
| | | :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" |
| | |
| | | </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> |
| | |
| | | // 获取班次字典值 |
| | | 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({ |
| | |
| | | }); |
| | | |
| | | // 表单验证规则 |
| | | const rules = { |
| | | 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" }], |
| | | 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)) { |
| | | if (form.shift && ["夜班", "夜"].includes(form.shift)) { |
| | | callback(); |
| | | return; |
| | | } |
| | |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // 部门选项 |
| | | const deptOptions = ref([]); |
| | |
| | | watch( |
| | | () => form.radius, |
| | | newValue => { |
| | | if (circle) { |
| | | circle.setRadius(newValue); |
| | | if (circle && newValue != null && newValue !== "") { |
| | | circle.setRadius(Number(newValue)); |
| | | } |
| | | } |
| | | ); |
| | |
| | | () => { |
| | | 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", |
| | | ]); |
| | | } |
| | | } |
| | | ); |
| | |
| | | |
| | | // 初始化地图 |
| | | setTimeout(() => { |
| | | if (!hideClockFields.value) { |
| | | initMap(); |
| | | } |
| | | }, 100); |
| | | } |
| | | } |
| | |
| | | </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 |
| | | 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> |
| | | <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> |
| | |
| | | 重置 |
| | | </el-button> |
| | | </el-form-item> |
| | | </el-form> --> |
| | | </el-form> |
| | | <!-- 班次列表 --> |
| | | <el-card shadow="never" |
| | | class="mb16"> |
| | |
| | | </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="操作" |
| | |
| | | <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> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted } from "vue"; |
| | | import { ref, reactive, onMounted, getCurrentInstance } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { |
| | | Plus, |
| | |
| | | |
| | | // 查询表单 |
| | | const searchForm = reactive({ |
| | | countId: "", |
| | | locationName: "", |
| | | sysDeptId: "", |
| | | shift: "", |
| | | }); |
| | | |
| | | // 部门选项 |
| | |
| | | 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); |
| | | }; |
| | | |
| | | // 获取部门列表 |
| | |
| | | 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(); |
| | | }; |
| | | |
| | | // 分页变更 |
| | |
| | | |
| | | // 重置搜索 |
| | | 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; |
| | |
| | | |
| | | // 删除班次 |
| | | const handleDelete = id => { |
| | | const row = tableData.value.find(item => String(item.id) === String(id)); |
| | | if (isRuleLocked(row)) { |
| | | ElMessage.warning("存在已排班且已有打卡记录的班次配置,不能删除"); |
| | | return; |
| | | } |
| | | ElMessageBox.confirm("确定要删除这条班次吗?", "删除确认", { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | |
| | | :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> |
| | |
| | | </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> |
| | |
| | | <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"> |
| | |
| | | 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" |
| | |
| | | <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"> |
| | |
| | | </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, |
| | |
| | | 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"; |
| | |
| | | |
| | | // 人员列表 |
| | | const personList = ref([]); |
| | | const personListAll = ref([]); |
| | | const leaveDateMap = ref({}); |
| | | |
| | | // 加载状态 |
| | | const loading = ref(false); |
| | |
| | | // 排班查询条件 |
| | | 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); |
| | | }); |
| | | }); |
| | | |
| | | // 列表数据 |
| | |
| | | proxy.$modal.msgError("请选择周次"); |
| | | return; |
| | | } |
| | | if (!schedulingQuery.deptId) { |
| | | proxy.$modal.msgError("请选择部门"); |
| | | return; |
| | | } |
| | | let time = schedulingQuery.week.getTime(); |
| | | |
| | | // 格式化日期为 YYYY-MM-DD 格式 |
| | |
| | | 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, |
| | |
| | | proxy.$modal.msgSuccess("操作成功"); |
| | | schedulingVisible.value = false; |
| | | schedulingQuery.week = ""; |
| | | schedulingQuery.deptId = ""; |
| | | schedulingQuery.userId = null; |
| | | schedulingQuery.shift = ""; |
| | | personList.value = []; |
| | | refresh(); |
| | | }) |
| | | .catch(err => { |
| | |
| | | } |
| | | 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("操作成功"); |
| | |
| | | // 查询规则列表 |
| | | 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; |
| | | }); |
| | | }; |
| | | // 获取用户 |
| | |
| | | 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(); |
| | | }; |
| | | |
| | | // 根据字典获取日期 |
| | |
| | | onMounted(() => { |
| | | fetchData(); |
| | | getUsers(); |
| | | getLeaveDates(); |
| | | fetchDeptOptions(); |
| | | if (query.month) { |
| | | init(); |
| | |
| | | } |
| | | 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> |