|
|
@@ -0,0 +1,1581 @@
|
|
|
+<template>
|
|
|
+ <main class="attendance-page rule-page" v-loading="loading">
|
|
|
+ <section class="workspace-card rule-card">
|
|
|
+ <div class="filter-bar filter-bar--rule">
|
|
|
+ <el-input v-model.trim="filter.keyword" prefix-icon="el-icon-search" clearable placeholder="打卡规则名称" />
|
|
|
+ <el-select v-model="filter.method" placeholder="打卡方式" clearable>
|
|
|
+ <el-option v-for="m in methodOptions" :key="m.value" :label="m.label" :value="m.value" />
|
|
|
+ </el-select>
|
|
|
+ <el-select v-model="filter.scope" placeholder="所属机构" clearable>
|
|
|
+ <el-option v-for="o in orgOptions" :key="o.value" :label="o.label" :value="o.value" />
|
|
|
+ </el-select>
|
|
|
+ <el-select v-model="filter.type" placeholder="规则类型" clearable>
|
|
|
+ <el-option v-for="t in ruleTypes" :key="t.value" :label="t.label" :value="t.value" />
|
|
|
+ </el-select>
|
|
|
+ <div class="filter-actions">
|
|
|
+ <el-button type="primary" icon="el-icon-search" @click="applyFilters">查询</el-button>
|
|
|
+ <el-button icon="el-icon-refresh-left" @click="resetFilters">重置</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <ele-pro-table ref="table" row-key="id" :columns="columns" :datasource="filteredRecords" :need-page="true"
|
|
|
+ :page-size="10" :toolkit="['size', 'columns', 'fullscreen']" stripe class="rule-table">
|
|
|
+ <template slot="toolbar">
|
|
|
+ <el-button size="small" type="primary" icon="el-icon-plus" @click="openCreate">新增打卡规则</el-button>
|
|
|
+ </template>
|
|
|
+ <template v-slot:name="{ row }">
|
|
|
+ <div class="name-cell">
|
|
|
+ <strong>{{ row.name }}</strong>
|
|
|
+ <small>{{ row.id }} · {{ scopeLabel(row.scope) }}</small>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-slot:type="{ row }"><span class="tag-pill" :class="typeTone(row.type)">{{ ruleTypeLabel(row.type)
|
|
|
+ }}</span></template>
|
|
|
+ <template v-slot:workTime="{ row }">{{ row.workTime }}</template>
|
|
|
+ <template v-slot:holiday="{ row }">{{ row.holiday }}</template>
|
|
|
+ <template v-slot:restDays="{ row }">{{ row.restDays.join("、") }}</template>
|
|
|
+ <template v-slot:methods="{ row }">
|
|
|
+ <div class="method-cell">
|
|
|
+ <span v-for="m in row.methods" :key="m" class="tag-pill" :class="methodTone(m)">{{ methodLabel(m) }}</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-slot:location="{ row }">{{ row.location }}</template>
|
|
|
+ <template v-slot:wifi="{ row }">{{ row.wifi }}</template>
|
|
|
+ <template v-slot:devices="{ row }">{{ row.devices }}</template>
|
|
|
+ <template v-slot:overtime="{ row }">
|
|
|
+ <span class="status-pill" :class="row.overtime === '开启' ? 'success' : 'muted'">
|
|
|
+ <i></i>{{ row.overtime }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ <template v-slot:overtimeDuration="{ row }"><small class="cell-multiline">{{ row.overtimeDuration
|
|
|
+ }}</small></template>
|
|
|
+ <template v-slot:makeup="{ row }"><span class="tag-pill"
|
|
|
+ :class="row.makeup === '允许' ? 'tag-green' : 'tag-gray'">{{ row.makeup }}</span></template>
|
|
|
+ <template v-slot:leaveMakeup="{ row }">{{ row.leaveMakeup }}</template>
|
|
|
+ <template v-slot:status="{ row }"><el-switch :value="row.enabled" @change="toggle(row)" /></template>
|
|
|
+ <template v-slot:action="{ row }">
|
|
|
+ <el-button type="text" size="mini" @click="viewDetail(row)">查看</el-button>
|
|
|
+ <el-button type="text" size="mini" @click="openEdit(row)">编辑</el-button>
|
|
|
+ <el-button type="text" size="mini" class="is-danger" @click="handleDelete(row)">删除</el-button>
|
|
|
+ </template>
|
|
|
+ <template slot="empty">
|
|
|
+ <el-button size="small" type="primary" @click="openCreate">新增打卡规则</el-button>
|
|
|
+ </template>
|
|
|
+ </ele-pro-table>
|
|
|
+ </section>
|
|
|
+ <ele-modal :title="dialogTitle" :visible.sync="dialogVisible" width="960px"
|
|
|
+ custom-class="rule-dialog ele-dialog-form"
|
|
|
+ :close-on-click-modal="false" :maxable="true" append-to-body>
|
|
|
+ <div class="dialog-intro">
|
|
|
+ <div class="intro-icon"><i class="el-icon-time"></i></div>
|
|
|
+ <div>
|
|
|
+ <strong>{{ dialogTitle === '编辑打卡规则' ? '维护打卡规则' : '创建新的打卡规则' }}</strong>
|
|
|
+ <p>配置打卡方式、上下班时间、节假日、加班与补卡等规则,保存后同步到考勤策略。</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="rule-dialog-tabs">
|
|
|
+ <button v-for="t in dialogTabs" :key="t.key" :class="['tab-chip', { active: activeTab === t.key }]"
|
|
|
+ type="button" @click="activeTab = t.key">{{ t.label }}</button>
|
|
|
+ </div>
|
|
|
+ <div class="rule-dialog-body">
|
|
|
+ <rule-form-panels
|
|
|
+ :form="form"
|
|
|
+ :active-tab="activeTab"
|
|
|
+ :week-options="weekOptions"
|
|
|
+ :time-options="timeOptions"
|
|
|
+ :related-target-options="relatedTargetOptions"
|
|
|
+ :assistant-user-options="assistantUserOptions"
|
|
|
+ :timezone-options="timezoneOptions"
|
|
|
+ :reminder-options="reminderOptions"
|
|
|
+ :rules="rules"
|
|
|
+ @add-location="addLocation"
|
|
|
+ @add-wifi="addWifi"
|
|
|
+ @add-device="openDeviceCard"
|
|
|
+ @device-view="viewDevice"
|
|
|
+ @device-edit="editDevice"
|
|
|
+ @device-test="testDevice"
|
|
|
+ @device-sync="syncDevice"
|
|
|
+ @device-restart="restartDevice"
|
|
|
+ @device-delete="deleteDevice"
|
|
|
+ @device-log="viewDeviceLog"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <span slot="footer">
|
|
|
+ <el-button @click="dialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="save">保存规则</el-button>
|
|
|
+ </span>
|
|
|
+ </ele-modal>
|
|
|
+
|
|
|
+ <!-- 添加打卡位置 -->
|
|
|
+ <ele-modal title="添加打卡位置" :visible.sync="locationDialogVisible" width="420px" custom-class="modal"
|
|
|
+ :close-on-click-modal="false" append-to-body>
|
|
|
+ <el-form :model="locationForm" label-position="top">
|
|
|
+ <el-form-item label="地点名称" required>
|
|
|
+ <el-input v-model.trim="locationForm.name" placeholder="如:麓谷企业广场F2栋" clearable />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="有效范围" required>
|
|
|
+ <el-select v-model="locationForm.range" placeholder="选择有效范围" style="width:100%">
|
|
|
+ <el-option v-for="r in rangeOptions" :key="r" :label="r" :value="r" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <span slot="footer">
|
|
|
+ <el-button @click="locationDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="confirmLocation">确定</el-button>
|
|
|
+ </span>
|
|
|
+ </ele-modal>
|
|
|
+
|
|
|
+ <!-- 添加打卡Wi-Fi -->
|
|
|
+ <ele-modal title="添加打卡Wi-Fi" :visible.sync="wifiDialogVisible" width="420px" custom-class="modal"
|
|
|
+ :close-on-click-modal="false" append-to-body>
|
|
|
+ <el-form :model="wifiForm" label-position="top">
|
|
|
+ <el-form-item label="WiFi 名称" required>
|
|
|
+ <el-input v-model.trim="wifiForm.ssid" placeholder="如:AIMILL-OFFICE" clearable />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="MAC 地址" required>
|
|
|
+ <el-input v-model.trim="wifiForm.bssid" placeholder="如:00:1A:2B:3C:4D:5E" clearable />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <span slot="footer">
|
|
|
+ <el-button @click="wifiDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="confirmWifi">确定</el-button>
|
|
|
+ </span>
|
|
|
+ </ele-modal>
|
|
|
+
|
|
|
+ <!-- 新增/编辑考勤机 -->
|
|
|
+ <ele-modal :title="deviceDialogTitle" :visible.sync="deviceDialogVisible" width="520px" custom-class="modal"
|
|
|
+ :close-on-click-modal="false" append-to-body>
|
|
|
+ <el-form :model="deviceForm" :rules="deviceRules" ref="deviceFormRef" label-position="top">
|
|
|
+ <el-form-item label="设备名称" prop="name">
|
|
|
+ <el-input v-model.trim="deviceForm.name" placeholder="如:前台考勤机" clearable />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="设备SN码" prop="sn">
|
|
|
+ <el-input v-model.trim="deviceForm.sn" placeholder="如:AIM-2025-0001" clearable />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="通信端口" prop="port">
|
|
|
+ <el-input-number v-model="deviceForm.port" :min="1" :max="65535" controls-position="right"
|
|
|
+ style="width:100%" placeholder="默认 4370" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="设备类型" prop="deviceType">
|
|
|
+ <el-select v-model="deviceForm.deviceType" placeholder="选择设备类型" style="width:100%">
|
|
|
+ <el-option v-for="t in deviceTypeOptions" :key="t" :label="t" :value="t" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="安装位置">
|
|
|
+ <el-input v-model.trim="deviceForm.location" placeholder="如:麓谷企业广场F2栋 1楼前台" clearable />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="备注">
|
|
|
+ <el-input v-model="deviceForm.remark" type="textarea" :rows="2" placeholder="选填" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <span slot="footer">
|
|
|
+ <el-button @click="deviceDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="confirmDevice">确定</el-button>
|
|
|
+ </span>
|
|
|
+ </ele-modal>
|
|
|
+
|
|
|
+ <!-- 设备详情 / 日志 共用查看弹窗 -->
|
|
|
+ <ele-modal :title="viewDialogTitle" :visible.sync="viewDialogVisible" width="520px" custom-class="modal"
|
|
|
+ :close-on-click-modal="false" append-to-body>
|
|
|
+ <div v-if="viewingDevice" class="device-view">
|
|
|
+ <template v-if="viewMode === 'detail'">
|
|
|
+ <div class="view-row"><span>设备名称</span><strong>{{ viewingDevice.name }}</strong></div>
|
|
|
+ <div class="view-row"><span>SN码</span><strong>{{ viewingDevice.sn }}</strong></div>
|
|
|
+ <div class="view-row"><span>IP地址</span><strong>{{ viewingDevice.ip }}</strong></div>
|
|
|
+ <div class="view-row"><span>通信端口</span><strong>{{ viewingDevice.port }}</strong></div>
|
|
|
+ <div class="view-row"><span>设备类型</span><strong>{{ viewingDevice.deviceType }}</strong></div>
|
|
|
+ <div class="view-row"><span>安装位置</span><strong>{{ viewingDevice.location || '—' }}</strong></div>
|
|
|
+ <div class="view-row"><span>在线状态</span><strong>{{ viewingDevice.online ? '在线' : '离线' }}</strong></div>
|
|
|
+ <div class="view-row"><span>在线时长</span><strong>{{ viewingDevice.onlineDuration }}</strong></div>
|
|
|
+ <div class="view-row"><span>绑定部门</span><strong>{{ viewingDevice.department }}</strong></div>
|
|
|
+ <div class="view-row"><span>最后同步时间</span><strong>{{ viewingDevice.lastSync }}</strong></div>
|
|
|
+ <div class="view-row"><span>备注</span><strong>{{ viewingDevice.remark || '—' }}</strong></div>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <div class="device-log-list">
|
|
|
+ <div v-for="(log, i) in deviceLogs" :key="i" class="log-item">
|
|
|
+ <span class="log-time">{{ log.time }}</span>
|
|
|
+ <span class="log-tag" :class="log.level">{{ log.levelText }}</span>
|
|
|
+ <span class="log-msg">{{ log.msg }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ <span slot="footer">
|
|
|
+ <el-button @click="viewDialogVisible = false">关闭</el-button>
|
|
|
+ </span>
|
|
|
+ </ele-modal>
|
|
|
+
|
|
|
+ <el-drawer custom-class="rule-drawer" :title="detailRow ? '打卡规则详情 - ' + detailRow.name : ''" :visible.sync="detailVisible" direction="rtl"
|
|
|
+ size="640px" append-to-body>
|
|
|
+ <div v-if="detailRow" style="padding:0 18px 18px;">
|
|
|
+ <div class="employee-summary">
|
|
|
+ <span><i class="el-icon-time"></i></span>
|
|
|
+ <div>
|
|
|
+ <h3>{{ detailRow.name }}</h3>
|
|
|
+ <p>{{ detailRow.id }} · {{ scopeLabel(detailRow.scope) }} · {{ ruleTypeLabel(detailRow.type) }}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="info-grid info-grid--two">
|
|
|
+ <div><span>上下班时间</span><strong>{{ detailRow.workTime }}</strong></div>
|
|
|
+ <div><span>节假日与特殊日期</span><strong>{{ detailRow.holiday }}</strong></div>
|
|
|
+ <div><span>休息日</span><strong>{{ detailRow.restDays.join("、") }}</strong></div>
|
|
|
+ <div><span>打卡方式</span><strong>{{ detailRow.methods.map(methodLabel).join("、") }}</strong></div>
|
|
|
+ <div><span>打卡位置</span><strong>{{ detailRow.location }}</strong></div>
|
|
|
+ <div><span>打卡WiFi</span><strong>{{ detailRow.wifi }}</strong></div>
|
|
|
+ <div><span>考勤机</span><strong>{{ detailRow.devices }}</strong></div>
|
|
|
+ <div><span>加班规则</span><strong>{{ detailRow.overtime }}</strong></div>
|
|
|
+ <div><span>加班时长设置</span><strong>{{ detailRow.overtimeDuration }}</strong></div>
|
|
|
+ <div><span>补卡设置</span><strong>{{ detailRow.makeup }}</strong></div>
|
|
|
+ <div><span>请假时补卡设置</span><strong>{{ detailRow.leaveMakeup }}</strong></div>
|
|
|
+ <div><span>最近更新</span><strong>{{ detailRow.updatedAt }}</strong></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-drawer>
|
|
|
+ </main>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { PUNCH_RULES, PUNCH_METHOD_OPTIONS, ORG_OPTIONS, RULE_TYPES, ruleTypeLabel, methodLabel, methodTone } from '../mock';
|
|
|
+import RuleFormPanels from '../components/RuleFormPanels.vue';
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'AttendanceRule',
|
|
|
+ components: { RuleFormPanels },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ loading: false,
|
|
|
+ methodOptions: PUNCH_METHOD_OPTIONS,
|
|
|
+ orgOptions: ORG_OPTIONS,
|
|
|
+ ruleTypes: RULE_TYPES,
|
|
|
+ filter: { keyword: '', method: '', scope: '', type: '' },
|
|
|
+ records: PUNCH_RULES.map(r => ({ ...r })),
|
|
|
+ detailRow: null,
|
|
|
+ detailVisible: false,
|
|
|
+ locationDialogVisible: false,
|
|
|
+ wifiDialogVisible: false,
|
|
|
+ locationForm: { name: '', range: '300米' },
|
|
|
+ wifiForm: { ssid: '', bssid: '' },
|
|
|
+ deviceDialogVisible: false,
|
|
|
+ deviceDialogTitle: '新增考勤机',
|
|
|
+ editingDeviceIndex: -1,
|
|
|
+ deviceForm: this.emptyDevice(),
|
|
|
+ deviceRules: {
|
|
|
+ name: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
|
|
|
+ sn: [{ required: true, message: '请输入设备SN码', trigger: 'blur' }],
|
|
|
+ port: [{ required: true, message: '请输入通信端口', trigger: 'blur' }],
|
|
|
+ deviceType: [{ required: true, message: '请选择设备类型', trigger: 'change' }]
|
|
|
+ },
|
|
|
+ deviceTypeOptions: ['考勤机', '考勤门禁一体机', '人脸识别考勤机', '指纹考勤机', '刷卡考勤机', '混合识别考勤机'],
|
|
|
+ viewDialogVisible: false,
|
|
|
+ viewDialogTitle: '设备详情',
|
|
|
+ viewingDevice: null,
|
|
|
+ viewMode: 'detail',
|
|
|
+ deviceLogs: [],
|
|
|
+ departmentOptions: ['总部', '研发中心', '市场部', '运营部', '人事行政部', '财务部'],
|
|
|
+ dialogVisible: false,
|
|
|
+ dialogTitle: '新增打卡规则',
|
|
|
+ activeTab: 'method',
|
|
|
+ dialogTabs: [
|
|
|
+ { key: 'method', label: '打卡方式' },
|
|
|
+ { key: 'workTime', label: '上下班时间' },
|
|
|
+ { key: 'rest', label: '节假日' },
|
|
|
+ { key: 'overtime', label: '加班规则' },
|
|
|
+ { key: 'makeup', label: '补卡设置' },
|
|
|
+ { key: 'leave', label: '请假规则' },
|
|
|
+ { key: 'assistant', label: '助理管理设置' },
|
|
|
+ { key: 'outside', label: '外出打卡' }
|
|
|
+ ],
|
|
|
+ form: this.empty(),
|
|
|
+ rangeOptions: ['50米', '100米', '150米', '200米', '300米', '500米', '800米', '1000米'],
|
|
|
+ timeOptions: ["00:00","00:30","01:00","01:30","02:00","02:30","03:00","03:30","04:00","04:30","05:00","05:30","06:00","06:30","07:00","07:30","08:00","08:30","09:00","09:30","10:00","10:30","11:00","11:30","12:00","12:30","13:00","13:30","14:00","14:30","15:00","15:30","16:00","16:30","17:00","17:30","18:00","18:30","19:00","19:30","20:00","20:30","21:00","21:30","22:00","22:30","23:00","23:30","24:00"],
|
|
|
+ weekOptions: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
|
|
+ relatedTargetOptions: [
|
|
|
+ { value: 'ding', label: '钉钉考勤' },
|
|
|
+ { value: 'wecom', label: '企业微信' },
|
|
|
+ { value: 'device', label: '考勤机' },
|
|
|
+ { value: 'app', label: '自有 App' }
|
|
|
+ ],
|
|
|
+ punchWindowOptions: [
|
|
|
+ { value: '0-24', label: '全天 00:00 - 24:00' },
|
|
|
+ { value: '5-24', label: '05:00 - 24:00' },
|
|
|
+ { value: '6-22', label: '06:00 - 22:00' },
|
|
|
+ { value: '7-20', label: '07:00 - 20:00' }
|
|
|
+ ],
|
|
|
+ assistantUserOptions: ['李亚洪', '王小云', '赵磊', '钱敏', '徐建国', '黄芳'],
|
|
|
+ timezoneOptions: [
|
|
|
+ '(GMT+08:00)中国标准时间-北京',
|
|
|
+ '(GMT+00:00)世界标准时间 UTC',
|
|
|
+ '(GMT-05:00)美国东部时间-纽约',
|
|
|
+ '(GMT+09:00)日本标准时间-东京',
|
|
|
+ '(GMT+05:30)印度标准时间-新德里'
|
|
|
+ ],
|
|
|
+ reminderOptions: [
|
|
|
+ '上班前 10 分钟、下班准点',
|
|
|
+ '上班前 5 分钟、下班延后 5 分钟',
|
|
|
+ '仅上班前 10 分钟',
|
|
|
+ '仅下班延后 5 分钟',
|
|
|
+ '不提醒'
|
|
|
+ ],
|
|
|
+ halfDayOptions: [
|
|
|
+ { value: 'morning', label: '上午班 08:00 - 12:00' },
|
|
|
+ { value: 'afternoon', label: '下午班 13:00 - 18:00' },
|
|
|
+ { value: 'custom-am', label: '上午半天 07:30 - 12:00' },
|
|
|
+ { value: 'custom-pm', label: '下午半天 12:30 - 17:30' }
|
|
|
+ ],
|
|
|
+ rules: {
|
|
|
+ name: [{ required: true, message: '请输入打卡规则名称', trigger: 'blur' }],
|
|
|
+ type: [{ required: true, message: '请选择规则类型', trigger: 'change' }],
|
|
|
+ scope: [{ required: true, message: '请选择所属机构', trigger: 'change' }]
|
|
|
+ },
|
|
|
+ page: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ columns: [
|
|
|
+ { type: 'index', label: '序号', width: 60, align: 'center', headerAlign: 'center' },
|
|
|
+ { prop: 'name', label: '打卡规则名称', minWidth: 200, slot: 'name' },
|
|
|
+ { prop: 'type', label: '规则类型', minWidth: 110, slot: 'type' },
|
|
|
+ { prop: 'workTime', label: '上下班时间', minWidth: 200, slot: 'workTime' },
|
|
|
+ { prop: 'holiday', label: '节假日与特殊日期', minWidth: 200, slot: 'holiday' },
|
|
|
+ { prop: 'restDays', label: '休息日', minWidth: 140, slot: 'restDays' },
|
|
|
+ { prop: 'methods', label: '打卡方式', minWidth: 180, slot: 'methods' },
|
|
|
+ { prop: 'location', label: '打卡位置', minWidth: 200, slot: 'location' },
|
|
|
+ { prop: 'wifi', label: '打卡WiFi', minWidth: 160, slot: 'wifi' },
|
|
|
+ { prop: 'devices', label: '考勤机', minWidth: 200, slot: 'devices' },
|
|
|
+ { prop: 'overtime', label: '加班规则', minWidth: 90, slot: 'overtime', align: 'center', headerAlign: 'center' },
|
|
|
+ { prop: 'overtimeDuration', label: '加班时长设置', minWidth: 220, slot: 'overtimeDuration' },
|
|
|
+ { prop: 'makeup', label: '补卡设置', minWidth: 100, slot: 'makeup', align: 'center', headerAlign: 'center' },
|
|
|
+ { prop: 'leaveMakeup', label: '请假时补卡设置', minWidth: 130, slot: 'leaveMakeup' },
|
|
|
+ { prop: 'action', label: '操作', width: 180, slot: 'action', align: 'left', headerAlign: 'left' }
|
|
|
+ ]
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ filteredRecords() {
|
|
|
+ return this.records.filter(r => {
|
|
|
+ if (this.filter.keyword && !r.name.includes(this.filter.keyword)) return false;
|
|
|
+ if (this.filter.method && !r.methods.includes(this.filter.method)) return false;
|
|
|
+ if (this.filter.scope && r.scope !== this.filter.scope) return false;
|
|
|
+ if (this.filter.type && r.type !== this.filter.type) return false;
|
|
|
+ return true;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ enabledCount() {
|
|
|
+ return this.records.filter(r => r.enabled).length;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ ruleTypeLabel,
|
|
|
+ methodLabel,
|
|
|
+ methodTone,
|
|
|
+ scopeLabel(scope) {
|
|
|
+ const o = this.orgOptions.find(x => x.value === scope);
|
|
|
+ return o ? o.label : scope;
|
|
|
+ },
|
|
|
+ typeTone(type) {
|
|
|
+ return ({ fixed: 'tag-blue', flexible: 'tag-purple', comprehensive: 'tag-amber' })[type] || 'tag-gray';
|
|
|
+ },
|
|
|
+ empty() {
|
|
|
+ return {
|
|
|
+ id: '', name: '', scope: 'HQ', type: 'fixed',
|
|
|
+ workStart: '09:00', workEnd: '18:00', crossDay: false,
|
|
|
+ holiday: '跟随国家法定节假日', restDays: ['周六', '周日'],
|
|
|
+ method: 'phone', locations: [], outOfRange: '异常',
|
|
|
+ wifis: [], devices: [], overtimeUnit: '小时',
|
|
|
+ overtimeRound: '四舍五入',
|
|
|
+ overtimeDecimal: '1位小数',
|
|
|
+ overtimeDayHours: 9,
|
|
|
+ overtime: '开启',
|
|
|
+ workOvertimeEnabled: true,
|
|
|
+ workOvertimePeriod: '下班后到次日上班前',
|
|
|
+ workCalcMethod: '按打卡时长计算',
|
|
|
+ workDeductRest: false,
|
|
|
+ workCompOrPay: true,
|
|
|
+ restOvertimeEnabled: true,
|
|
|
+ restOvertimePeriod: '全天',
|
|
|
+ restCalcMethod: '按审批时长计算',
|
|
|
+ restDeductRest: false,
|
|
|
+ restCompOrPay: true,
|
|
|
+ holidayOvertimeEnabled: true,
|
|
|
+ holidayOvertimePeriod: '全天',
|
|
|
+ holidayCalcMethod: '按审批时长计算',
|
|
|
+ holidayDeductRest: false,
|
|
|
+ holidayCompOrPay: true,
|
|
|
+ overtimeStart: 30, overtimeDuration: '平日 1.5x / 周末 2x / 节假日 3x',
|
|
|
+ makeupEnabled: true,
|
|
|
+ makeupType: '缺卡/旷工',
|
|
|
+ makeupTimeLimit: '不限制',
|
|
|
+ makeupReminder: '启用',
|
|
|
+ makeupReminderTime: '09:00',
|
|
|
+ makeupMaxPerMonth: '3次',
|
|
|
+ makeupDeadline: '不设置',
|
|
|
+ approvalPunchEnabled: true,
|
|
|
+ workDays: ['周一', '周二', '周三', '周四', '周五'],
|
|
|
+ workShifts: [{ key: 'WS1', start: '09:00', end: '18:00', startOpen: '是', endOpen: '是', lunchBreaks: [{ key: 'L1', start: '12:00', end: '13:00' }] }],
|
|
|
+ relatedTarget: 'ding',
|
|
|
+ punchWindow: '5-24',
|
|
|
+ punchStart: '05:00',
|
|
|
+ punchEnd: '24:00',
|
|
|
+ flexEnabled: false,
|
|
|
+ lateComp: false,
|
|
|
+ halfDayRange: 'morning',
|
|
|
+ halfStart: '08:00',
|
|
|
+ halfEnd: '12:00',
|
|
|
+ leaveNeedPunch: true,
|
|
|
+ leavePunchWindow: '不限制',
|
|
|
+ leaveRules: [
|
|
|
+ { category: '请假', name: '年假', type: '带薪假', scope: '全体员工', condition: '在职年限≥1年', quota: '5天', salary: '不扣薪资', approval: '待审核', status: '未生效', action: '修改' },
|
|
|
+ { category: '请假', name: '事假', type: '扣除薪资', scope: '全体员工', condition: '入职就可', quota: '0天', salary: '按天扣除', approval: '已审核', status: '已生效', action: '已归档' },
|
|
|
+ { category: '请假', name: '病假', type: '带薪假', scope: '全体员工', condition: '入职就可', quota: '0.3天', salary: '按天扣除40%', approval: '已审核', status: '已生效', action: '已归档' },
|
|
|
+ { category: '请假', name: '婚假', type: '特殊假期', scope: '全体员工', condition: '入职就可', quota: '10天', salary: '不扣薪资', approval: '已审核', status: '已生效', action: '已归档' },
|
|
|
+ { category: '请假', name: '调休', type: '加班调休', scope: '全体员工', condition: '入职就可', quota: '0天', salary: '不扣薪资', approval: '已审核', status: '已生效', action: '已归档' },
|
|
|
+ { category: '请假', name: '产假', type: '特殊假期', scope: '转正员工', condition: '入职就可', quota: '25天', salary: '不扣薪资', approval: '已审核', status: '已生效', action: '已归档' }
|
|
|
+ ],
|
|
|
+ assistantEnabled: true,
|
|
|
+ assistantExemptUsers: [],
|
|
|
+ timezone: '(GMT+08:00)中国标准时间-北京',
|
|
|
+ reminderOffset: '上班前 10 分钟、下班准点',
|
|
|
+ asstOutOfRange: '记录为地点异常',
|
|
|
+ faceRecognition: '启用',
|
|
|
+ photoEachPunch: false,
|
|
|
+ faceEachPunch: false,
|
|
|
+ outsourceSync: false,
|
|
|
+ outsideEnabled: true,
|
|
|
+ outsideMode: 'time',
|
|
|
+ outsideStart: '09:00',
|
|
|
+ outsideEnd: '18:00',
|
|
|
+ outsideAddress: '不限制',
|
|
|
+ outsidePhoto: true,
|
|
|
+ reportStart: '09:00',
|
|
|
+ reportEnd: '10:00',
|
|
|
+ enabled: true, updatedAt: ''
|
|
|
+ };
|
|
|
+ },
|
|
|
+ addLocation() {
|
|
|
+ this.locationForm = { name: '', range: '300米' };
|
|
|
+ this.locationDialogVisible = true;
|
|
|
+ },
|
|
|
+ confirmLocation() {
|
|
|
+ if (!this.locationForm.name) return this.$message.warning('请输入地点名称');
|
|
|
+ this.form.locations.push({ name: this.locationForm.name, range: this.locationForm.range });
|
|
|
+ this.locationDialogVisible = false;
|
|
|
+ this.$message.success('已添加打卡位置');
|
|
|
+ },
|
|
|
+ addWifi() {
|
|
|
+ this.wifiForm = { ssid: '', bssid: '' };
|
|
|
+ this.wifiDialogVisible = true;
|
|
|
+ },
|
|
|
+ confirmWifi() {
|
|
|
+ if (!this.wifiForm.ssid) return this.$message.warning('请输入 WiFi 名称');
|
|
|
+ if (!this.wifiForm.bssid) return this.$message.warning('请输入 MAC 地址');
|
|
|
+ this.form.wifis.push({ ssid: this.wifiForm.ssid, bssid: this.wifiForm.bssid });
|
|
|
+ this.wifiDialogVisible = false;
|
|
|
+ this.$message.success('已添加打卡Wi-Fi');
|
|
|
+ },
|
|
|
+ applyFilters() { },
|
|
|
+ resetFilters() { this.filter = { keyword: '', method: '', scope: '', type: '' }; },
|
|
|
+ exportRules() { this.$message.success('已导出打卡规则'); },
|
|
|
+ openCreate() {
|
|
|
+ this.form = this.empty();
|
|
|
+ this.activeTab = 'method';
|
|
|
+ this.dialogTitle = '新增打卡规则';
|
|
|
+ this.dialogVisible = true;
|
|
|
+ },
|
|
|
+ openEdit(row) {
|
|
|
+ const base = this.empty();
|
|
|
+ const formData = {
|
|
|
+ ...base,
|
|
|
+ ...row,
|
|
|
+ overtimeOn: row.overtime === '开启',
|
|
|
+ locations: row.location ? [{ name: row.location, range: '300米' }] : [],
|
|
|
+ wifis: row.wifi ? [{ ssid: row.wifi, bssid: '-' }] : []
|
|
|
+ };
|
|
|
+ // 老数据没有 workShifts 时,尝试从 workTime 字符串反解析
|
|
|
+ if (!formData.workShifts && row.workTime) {
|
|
|
+ const m = row.workTime.match(/(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
|
|
|
+ if (m) {
|
|
|
+ formData.workShifts = [{ key: 'WS1', start: m[1], end: m[2], startOpen: '是', endOpen: '是', lunchBreaks: [{ key: 'L1', start: '12:00', end: '13:00' }] }];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.form = formData;
|
|
|
+ this.activeTab = 'method';
|
|
|
+ this.dialogTitle = '编辑打卡规则';
|
|
|
+ this.dialogVisible = true;
|
|
|
+ },
|
|
|
+ toggle(row) {
|
|
|
+ row.enabled = !row.enabled;
|
|
|
+ this.$message.success('已' + (row.enabled ? '启用' : '停用') + ':' + row.name);
|
|
|
+ },
|
|
|
+ openDeviceCard() {
|
|
|
+ this.deviceForm = this.emptyDevice();
|
|
|
+ this.editingDeviceIndex = -1;
|
|
|
+ this.deviceDialogTitle = '新增考勤机';
|
|
|
+ this.deviceDialogVisible = true;
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (this.$refs.deviceFormRef) this.$refs.deviceFormRef.clearValidate();
|
|
|
+ });
|
|
|
+ },
|
|
|
+ confirmDevice() {
|
|
|
+ this.$refs.deviceFormRef.validate(valid => {
|
|
|
+ if (!valid) return;
|
|
|
+ const f = this.deviceForm;
|
|
|
+ if (this.editingDeviceIndex > -1) {
|
|
|
+ const target = this.form.devices[this.editingDeviceIndex];
|
|
|
+ Object.assign(target, {
|
|
|
+ name: f.name, sn: f.sn, port: f.port,
|
|
|
+ deviceType: f.deviceType, location: f.location, remark: f.remark
|
|
|
+ });
|
|
|
+ this.$message.success('已更新考勤机:' + f.name);
|
|
|
+ } else {
|
|
|
+ const now = new Date();
|
|
|
+ const lastSync = now.toISOString().slice(0, 19).replace('T', ' ');
|
|
|
+ const ip = '192.168.' + Math.floor(Math.random() * 250 + 1) + '.' + Math.floor(Math.random() * 250 + 1);
|
|
|
+ this.form.devices.push({
|
|
|
+ id: 'DEV' + String(Date.now()).slice(-6),
|
|
|
+ name: f.name,
|
|
|
+ sn: f.sn,
|
|
|
+ port: f.port || 4370,
|
|
|
+ deviceType: f.deviceType,
|
|
|
+ location: f.location,
|
|
|
+ remark: f.remark,
|
|
|
+ ip,
|
|
|
+ online: true,
|
|
|
+ onlineDuration: '0小时0分钟',
|
|
|
+ department: this.departmentOptions[Math.floor(Math.random() * this.departmentOptions.length)],
|
|
|
+ lastSync
|
|
|
+ });
|
|
|
+ this.$message.success('已添加考勤机:' + f.name);
|
|
|
+ }
|
|
|
+ this.deviceDialogVisible = false;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ emptyDevice() {
|
|
|
+ return { name: '', sn: '', port: 4370, deviceType: '', location: '', remark: '' };
|
|
|
+ },
|
|
|
+ viewDevice(row) {
|
|
|
+ this.viewingDevice = row;
|
|
|
+ this.viewMode = 'detail';
|
|
|
+ this.viewDialogTitle = '设备详情';
|
|
|
+ this.viewDialogVisible = true;
|
|
|
+ },
|
|
|
+ editDevice(row) {
|
|
|
+ const idx = this.form.devices.findIndex(d => d === row);
|
|
|
+ if (idx < 0) return;
|
|
|
+ this.editingDeviceIndex = idx;
|
|
|
+ this.deviceForm = { ...row };
|
|
|
+ this.deviceDialogTitle = '编辑考勤机';
|
|
|
+ this.deviceDialogVisible = true;
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (this.$refs.deviceFormRef) this.$refs.deviceFormRef.clearValidate();
|
|
|
+ });
|
|
|
+ },
|
|
|
+ testDevice(row) {
|
|
|
+ const loading = this.$message({ message: '正在测试连接 ' + row.name + ' ...', type: 'info', duration: 0 });
|
|
|
+ setTimeout(() => {
|
|
|
+ loading.close();
|
|
|
+ this.$message.success(row.name + ' 连接测试成功');
|
|
|
+ const target = this.form.devices.find(d => d === row);
|
|
|
+ if (target) {
|
|
|
+ target.online = true;
|
|
|
+ target.lastSync = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
|
+ }
|
|
|
+ }, 800);
|
|
|
+ },
|
|
|
+ syncDevice(row) {
|
|
|
+ const loading = this.$message({ message: '正在同步 ' + row.name + ' 的考勤数据...', type: 'info', duration: 0 });
|
|
|
+ setTimeout(() => {
|
|
|
+ loading.close();
|
|
|
+ this.$message.success(row.name + ' 数据同步完成');
|
|
|
+ const target = this.form.devices.find(d => d === row);
|
|
|
+ if (target) {
|
|
|
+ target.online = true;
|
|
|
+ target.lastSync = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
|
+ target.onlineDuration = this.bumpDuration(target.onlineDuration);
|
|
|
+ }
|
|
|
+ }, 1000);
|
|
|
+ },
|
|
|
+ restartDevice(row) {
|
|
|
+ this.$confirm('确认重启考勤机「' + row.name + '」?设备将在 30 秒后恢复在线。', '提示', { type: 'warning' })
|
|
|
+ .then(() => {
|
|
|
+ const target = this.form.devices.find(d => d === row);
|
|
|
+ if (target) target.online = false;
|
|
|
+ this.$message.success(row.name + ' 正在重启...');
|
|
|
+ setTimeout(() => {
|
|
|
+ if (target) {
|
|
|
+ target.online = true;
|
|
|
+ target.lastSync = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
|
+ target.onlineDuration = '0小时0分钟';
|
|
|
+ }
|
|
|
+ this.$message.success(row.name + ' 已重启完成');
|
|
|
+ }, 2000);
|
|
|
+ }).catch(() => { });
|
|
|
+ },
|
|
|
+ deleteDevice(row) {
|
|
|
+ this.$confirm('确认删除考勤机「' + row.name + '」?', '提示', { type: 'warning' })
|
|
|
+ .then(() => {
|
|
|
+ this.form.devices = this.form.devices.filter(d => d !== row);
|
|
|
+ this.$message.success('已删除考勤机:' + row.name);
|
|
|
+ }).catch(() => { });
|
|
|
+ },
|
|
|
+ viewDeviceLog(row) {
|
|
|
+ this.viewingDevice = row;
|
|
|
+ this.viewMode = 'log';
|
|
|
+ this.viewDialogTitle = row.name + ' - 操作日志';
|
|
|
+ this.deviceLogs = this.buildMockLogs(row);
|
|
|
+ this.viewDialogVisible = true;
|
|
|
+ },
|
|
|
+ buildMockLogs(row) {
|
|
|
+ const now = Date.now();
|
|
|
+ const mk = (offsetMin, level, levelText, msg) => {
|
|
|
+ const t = new Date(now - offsetMin * 60000);
|
|
|
+ return {
|
|
|
+ time: t.toISOString().slice(0, 19).replace('T', ' '),
|
|
|
+ level, levelText, msg
|
|
|
+ };
|
|
|
+ };
|
|
|
+ return [
|
|
|
+ mk(1, 'success', '成功', '设备 ' + row.name + ' 上传打卡记录 32 条'),
|
|
|
+ mk(5, 'info', '信息', '心跳包上报,设备运行正常'),
|
|
|
+ mk(15, 'success', '成功', '系统完成与 ' + row.sn + ' 的时钟同步'),
|
|
|
+ mk(38, 'warn', '告警', '检测到短时网络抖动,已自动重连'),
|
|
|
+ mk(60, 'info', '信息', '设备 ' + row.name + ' 启动完成,开始服务'),
|
|
|
+ mk(120, 'success', '成功', '固件版本校验通过(当前 v2.4.1)')
|
|
|
+ ];
|
|
|
+ },
|
|
|
+ bumpDuration(prev) {
|
|
|
+ // 把 "X小时Y分钟" + 1 分钟,返回新值
|
|
|
+ const m = (prev || '').match(/(\d+)小时(\d+)分钟/);
|
|
|
+ if (!m) return '0小时1分钟';
|
|
|
+ let h = parseInt(m[1], 10), mi = parseInt(m[2], 10) + 1;
|
|
|
+ if (mi >= 60) { h += 1; mi -= 60; }
|
|
|
+ return h + '小时' + mi + '分钟';
|
|
|
+ },
|
|
|
+ viewDetail(row) {
|
|
|
+ this.detailRow = row;
|
|
|
+ this.detailVisible = true;
|
|
|
+ },
|
|
|
+ save() {
|
|
|
+ const data = { ...this.form };
|
|
|
+ data.overtime = data.overtimeOn ? '开启' : '关闭';
|
|
|
+ const firstShift = (data.workShifts && data.workShifts[0]) || {};
|
|
|
+ data.workStart = firstShift.start || data.workStart || '';
|
|
|
+ data.workEnd = firstShift.end || data.workEnd || '';
|
|
|
+ data.crossDay = firstShift.crossDay || false;
|
|
|
+ data.workTime = (data.workStart || '') + ' - ' + (data.workEnd || '') + (data.crossDay ? '(跨日)' : '');
|
|
|
+ const methods = [];
|
|
|
+ if (data.method === 'phone' || data.method === 'phone-device') {
|
|
|
+ if (data.locations.length) methods.push('gps');
|
|
|
+ if (data.wifis.length) methods.push('wifi');
|
|
|
+ }
|
|
|
+ if (data.method === 'device' || data.method === 'phone-device') methods.push('face');
|
|
|
+ data.methods = methods.length ? methods : ['gps', 'face'];
|
|
|
+ data.location = data.locations.map(l => l.name + '(' + l.range + ')').join('、') || '-';
|
|
|
+ data.wifi = data.wifis.map(w => w.ssid).join('、') || '-';
|
|
|
+ data.updatedAt = new Date().toISOString().slice(0, 16).replace('T', ' ');
|
|
|
+ if (!data.id) {
|
|
|
+ data.id = 'PR' + String(Date.now()).slice(-4);
|
|
|
+ this.records.unshift(data);
|
|
|
+ this.$message.success('打卡规则已新增');
|
|
|
+ } else {
|
|
|
+ const idx = this.records.findIndex(r => r.id === data.id);
|
|
|
+ if (idx > -1) this.records.splice(idx, 1, data);
|
|
|
+ this.$message.success('打卡规则已保存');
|
|
|
+ }
|
|
|
+ this.dialogVisible = false;
|
|
|
+ },
|
|
|
+ handleDelete(row) {
|
|
|
+ this.$confirm('确认删除「' + row.name + '」?', '提示', { type: 'warning' })
|
|
|
+ .then(() => {
|
|
|
+ this.records = this.records.filter(r => r.id !== row.id);
|
|
|
+ this.$message.success('已删除');
|
|
|
+ }).catch(() => { });
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style>
|
|
|
+/* ===== dialog 主体(ele-modal custom-class="rule-dialog")===== */
|
|
|
+.rule-dialog .el-dialog__header {
|
|
|
+ padding: 16px 20px 14px;
|
|
|
+ border-bottom: 1px solid #edf0f5;
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .el-dialog__header .el-dialog__title {
|
|
|
+ color: #24344b;
|
|
|
+ font-size: 17px;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .el-dialog__headerbtn {
|
|
|
+ top: 16px;
|
|
|
+ right: 18px;
|
|
|
+ width: 30px;
|
|
|
+ height: 30px;
|
|
|
+ border-radius: 6px;
|
|
|
+ transition: background 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .el-dialog__headerbtn:hover {
|
|
|
+ background: #f1f4f7;
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .el-dialog__headerbtn .el-dialog__close {
|
|
|
+ color: #7b899a;
|
|
|
+ font-size: 17px;
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .el-dialog__body {
|
|
|
+ padding: 14px 18px 18px;
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .dialog-intro {
|
|
|
+ margin: 0 0 12px;
|
|
|
+ padding: 12px 15px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 14px;
|
|
|
+ border: 1px solid #dceaff;
|
|
|
+ border-radius: 12px;
|
|
|
+ background: linear-gradient(120deg, #f4f9ff, #f8fbff);
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .dialog-intro strong {
|
|
|
+ display: block;
|
|
|
+ color: #24344b;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .dialog-intro p {
|
|
|
+ margin: 5px 0 0;
|
|
|
+ color: #8390a5;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .intro-icon {
|
|
|
+ flex: none;
|
|
|
+ width: 38px;
|
|
|
+ height: 38px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border-radius: 11px;
|
|
|
+ color: #fff;
|
|
|
+ background: linear-gradient(135deg, #1877f2, #43a8ff);
|
|
|
+ box-shadow: 0 8px 20px rgba(24, 119, 242, 0.22);
|
|
|
+ font-size: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .rule-dialog-tabs {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: nowrap;
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 14px;
|
|
|
+ padding: 4px 0 10px;
|
|
|
+ border-bottom: 1px dashed #e1e7ef;
|
|
|
+ overflow-x: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-chip {
|
|
|
+ height: 32px;
|
|
|
+ min-width: auto;
|
|
|
+ padding: 0 16px;
|
|
|
+ border: 1px solid #cdd5df;
|
|
|
+ border-radius: 16px;
|
|
|
+ background: #f4f6fa;
|
|
|
+ color: #5a6573;
|
|
|
+ font-size: 13px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all .15s;
|
|
|
+ white-space: nowrap;
|
|
|
+ line-height: 30px;
|
|
|
+ outline: none;
|
|
|
+ margin: 0;
|
|
|
+ font-family: inherit
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-chip:hover {
|
|
|
+ color: #1b8eb1;
|
|
|
+ border-color: #1b8eb1;
|
|
|
+ background: #fff
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-chip.active {
|
|
|
+ color: #fff;
|
|
|
+ background: #1b8eb1;
|
|
|
+ border-color: #1b8eb1;
|
|
|
+ font-weight: 600;
|
|
|
+ box-shadow: 0 2px 6px rgba(27, 142, 177, .25)
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-chip.active:hover {
|
|
|
+ background: #157a99;
|
|
|
+ border-color: #157a99
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .rule-dialog-body {
|
|
|
+ display: flex;
|
|
|
+ gap: 18px;
|
|
|
+ min-height: 380px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .rule-form-col {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .rule-side-col {
|
|
|
+ width: 240px;
|
|
|
+ flex: none
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .form-section {
|
|
|
+ padding: 12px;
|
|
|
+ border: 1px solid #e1e7ef;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #fff;
|
|
|
+ margin-bottom: 12px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .form-section:last-child {
|
|
|
+ margin-bottom: 0
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .form-section header {
|
|
|
+ margin-bottom: 10px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .form-section header h3 {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #24344b
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .form-section header p {
|
|
|
+ margin: 4px 0 0;
|
|
|
+ color: #8a96a7;
|
|
|
+ font-size: 11px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .required {
|
|
|
+ color: #d55752;
|
|
|
+ margin-right: 2px;
|
|
|
+ font-weight: 700
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .form-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 0 16px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .form-grid--three {
|
|
|
+ grid-template-columns: repeat(3, 1fr)
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .el-form-item {
|
|
|
+ margin-bottom: 14px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .el-form-item__label {
|
|
|
+ padding-bottom: 4px;
|
|
|
+ color: #4a586d;
|
|
|
+ font-size: 11px;
|
|
|
+ line-height: 20px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .el-form-item .el-input,
|
|
|
+.rule-dialog .tab-panel .el-form-item .el-select,
|
|
|
+.rule-dialog .tab-panel .el-form-item .el-time-picker {
|
|
|
+ width: 100%
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .el-form-item .el-radio {
|
|
|
+ color: #3d4a5e;
|
|
|
+ font-size: 12px;
|
|
|
+ margin-right: 18px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .el-form-item .el-checkbox {
|
|
|
+ color: #3d4a5e;
|
|
|
+ font-size: 12px;
|
|
|
+ margin-right: 14px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .method-radio {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 6px 0
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .method-radio .el-radio {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ margin-right: 0;
|
|
|
+ line-height: 1.5
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .method-radio .el-radio__label {
|
|
|
+ color: #3d4a5e;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.6
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .location-table {
|
|
|
+ margin: 10px 0;
|
|
|
+ border: 1px solid #e1e7ef;
|
|
|
+ border-radius: 6px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .location-table .el-table__header th.el-table__cell {
|
|
|
+ background: #f6f8fb;
|
|
|
+ color: #68778c;
|
|
|
+ font-weight: 600;
|
|
|
+ height: 36px;
|
|
|
+ padding: 4px 0
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .location-table .el-table__body td.el-table__cell {
|
|
|
+ padding: 6px 0;
|
|
|
+ font-size: 12px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .range-form-item {
|
|
|
+ margin-top: 10px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .wifi-tip {
|
|
|
+ margin-top: 14px;
|
|
|
+ padding: 12px 14px;
|
|
|
+ background: #f8faff;
|
|
|
+ border: 1px solid #dfe7f0;
|
|
|
+ border-radius: 6px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .wifi-tip h4 {
|
|
|
+ margin: 0 0 8px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #24344b
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .wifi-tip ol {
|
|
|
+ margin: 0;
|
|
|
+ padding-left: 18px;
|
|
|
+ color: #6e7d91;
|
|
|
+ font-size: 11px;
|
|
|
+ line-height: 1.8
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .side-card {
|
|
|
+ padding: 16px;
|
|
|
+ border: 1px solid #e1e7ef;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: linear-gradient(180deg, #f8faff 0%, #fff 100%);
|
|
|
+ position: sticky;
|
|
|
+ top: 18px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .side-card h4 {
|
|
|
+ margin: 0 0 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #24344b
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .side-card p {
|
|
|
+ margin: 0 0 8px;
|
|
|
+ color: #6e7d91;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.6
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .side-card .side-desc {
|
|
|
+ color: #8995a5;
|
|
|
+ font-size: 11px;
|
|
|
+ margin-bottom: 14px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .form-hint {
|
|
|
+ margin-left: 8px;
|
|
|
+ color: #8d99a9;
|
|
|
+ font-size: 11px
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== drawer(el-drawer custom-class="rule-drawer")===== */
|
|
|
+.rule-drawer .employee-summary {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 11px;
|
|
|
+ padding: 14px;
|
|
|
+ border: 1px solid #dfe7f0;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: #f8faff;
|
|
|
+ margin-bottom: 18px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-drawer .employee-summary > span {
|
|
|
+ width: 46px;
|
|
|
+ height: 46px;
|
|
|
+ display: grid;
|
|
|
+ place-items: center;
|
|
|
+ border-radius: 50%;
|
|
|
+ color: #1768e5;
|
|
|
+ background: #e6efff;
|
|
|
+ font-size: 18px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-drawer .employee-summary h3 {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 15px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-drawer .employee-summary p {
|
|
|
+ margin: 5px 0 0;
|
|
|
+ color: #8390a2;
|
|
|
+ font-size: 11px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-drawer .employee-summary > div:nth-child(3) {
|
|
|
+ margin-left: auto;
|
|
|
+ text-align: right
|
|
|
+}
|
|
|
+
|
|
|
+.rule-drawer .employee-summary > div:nth-child(3) small,
|
|
|
+.rule-drawer .employee-summary > div:nth-child(3) strong {
|
|
|
+ display: block
|
|
|
+}
|
|
|
+
|
|
|
+.rule-drawer .employee-summary > div:nth-child(3) small {
|
|
|
+ color: #8995a5;
|
|
|
+ font-size: 10px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-drawer .employee-summary > div:nth-child(3) strong {
|
|
|
+ margin-top: 3px;
|
|
|
+ color: #1768e5;
|
|
|
+ font-size: 13px
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 上下班时间 Tab 附加样式 ===== */
|
|
|
+.rule-dialog .tab-panel .week-chip-wrap {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .week-chips {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .week-chips .el-checkbox-button {
|
|
|
+ width: auto;
|
|
|
+ margin: 0 !important
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .week-chips .el-checkbox-button__inner {
|
|
|
+ display: inline-block;
|
|
|
+ padding: 6px 18px;
|
|
|
+ border: 1px solid #d3dbe5 !important;
|
|
|
+ border-radius: 999px !important;
|
|
|
+ background: #fff;
|
|
|
+ color: #5a6573;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 18px;
|
|
|
+ box-shadow: none !important;
|
|
|
+ transition: all .15s
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .week-chips .el-checkbox-button:hover .el-checkbox-button__inner {
|
|
|
+ border-color: #1b8eb1;
|
|
|
+ color: #1b8eb1
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .week-chips .el-checkbox-button.is-checked .el-checkbox-button__inner {
|
|
|
+ background: #1b8eb1 !important;
|
|
|
+ border-color: #1b8eb1 !important;
|
|
|
+ color: #fff !important;
|
|
|
+ box-shadow: 0 2px 6px rgba(27, 142, 177, .25) !important
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .week-chips .el-checkbox-button.is-checked .el-checkbox-button__inner:hover {
|
|
|
+ background: #157a99 !important
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .lunch-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 10px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .lunch-row .dash {
|
|
|
+ color: #8d99a9;
|
|
|
+ font-size: 12px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .field-hint {
|
|
|
+ margin-left: 8px;
|
|
|
+ color: #8d99a9;
|
|
|
+ font-size: 11px
|
|
|
+}
|
|
|
+.rule-dialog .tab-panel .time-pair {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px
|
|
|
+}
|
|
|
+.rule-dialog .tab-panel .field-with-dropdown {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ width: 100%
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .field-with-dropdown .time-pick {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .field-with-dropdown .open-label {
|
|
|
+ flex: none;
|
|
|
+ color: #6e7d91;
|
|
|
+ font-size: 12px;
|
|
|
+ white-space: nowrap
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .field-with-dropdown .open-select {
|
|
|
+ flex: none;
|
|
|
+ width: 120px
|
|
|
+}
|
|
|
+/* ===== 外出打卡 Tab 附加样式 ===== */
|
|
|
+.rule-dialog .tab-panel .outside-mode {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .outside-mode .el-radio {
|
|
|
+ margin-right: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ line-height: 1.4
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .outside-subfield {
|
|
|
+ margin: 4px 0 14px 22px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .outside-subfield--row {
|
|
|
+ flex-wrap: wrap
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .outside-subfield-label {
|
|
|
+ color: #5a6573;
|
|
|
+ font-size: 12px;
|
|
|
+ white-space: nowrap
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .outside-photo-label {
|
|
|
+ color: #5a6573;
|
|
|
+ font-size: 12px;
|
|
|
+ white-space: nowrap;
|
|
|
+ margin-left: 8px
|
|
|
+}
|
|
|
+.rule-dialog .tab-panel .assistant-row {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 0 16px
|
|
|
+}
|
|
|
+/* ===== 补卡设置 Tab 附加样式 ===== */
|
|
|
+.rule-dialog .tab-panel .makeup-row {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 0 16px;
|
|
|
+ align-items: start
|
|
|
+}
|
|
|
+/* ===== 加班规则 Tab 附加样式 ===== */
|
|
|
+.rule-dialog .tab-panel .overtime-row {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr 1fr;
|
|
|
+ gap: 0 16px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .overtime-period-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ flex-wrap: wrap
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .overtime-period-row .el-form-item {
|
|
|
+ margin-bottom: 14px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .overtime-toggle-row {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 0 16px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .overtime-section-subtitle {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #4a586d;
|
|
|
+ font-weight: 600;
|
|
|
+ margin: 4px 0 8px;
|
|
|
+ padding-left: 8px;
|
|
|
+ border-left: 2px solid #1b8eb1
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .period-label {
|
|
|
+ color: #5a6573;
|
|
|
+ font-size: 12px;
|
|
|
+ white-space: nowrap
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+/* ===== 请假规则 Tab 附加样式 ===== */
|
|
|
+.rule-dialog .tab-panel .leave-rule-table {
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+ margin-bottom: 12px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .leave-rule-table .el-table__header th.el-table__cell {
|
|
|
+ background: #f6f8fb;
|
|
|
+ color: #2b3a52;
|
|
|
+ font-weight: 600
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .leave-rule-table .status-pill {
|
|
|
+ font-size: 11px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .leave-rule-table .el-button.is-archived {
|
|
|
+ color: #aab4c0;
|
|
|
+ cursor: default
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .leave-rule-table .el-button.is-archived:hover {
|
|
|
+ color: #aab4c0
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .leave-rule-actions {
|
|
|
+ text-align: right;
|
|
|
+ margin-top: 8px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .status-pill.pending {
|
|
|
+ color: #c67d23
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .status-pill.pending i {
|
|
|
+ background: #e6a23c
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .time-pair .dash {
|
|
|
+ color: #8d99a9;
|
|
|
+ font-size: 12px;
|
|
|
+ flex: none
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 工作时间(多条)样式 ===== */
|
|
|
+.rule-dialog .tab-panel .shift-card {
|
|
|
+ padding: 12px 14px;
|
|
|
+ border: 1px dashed #cdd5df;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #fbfcfe;
|
|
|
+ margin-bottom: 12px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .shift-card:first-of-type {
|
|
|
+ margin-top: 0
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .shift-head {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 10px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .rule-dialog .tab-panel .shift-no {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ color: #2b3a52;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .shift-no i {
|
|
|
+ display: grid;
|
|
|
+ place-items: center;
|
|
|
+ width: 22px;
|
|
|
+ height: 22px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: linear-gradient(135deg, #1b8eb1, #1768e5);
|
|
|
+ color: #fff;
|
|
|
+ font-size: 12px;
|
|
|
+ font-style: normal;
|
|
|
+ font-weight: 700;
|
|
|
+ box-shadow: 0 2px 5px rgba(23, 104, 229, .25)
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .lunch-block {
|
|
|
+ margin-top: 6px;
|
|
|
+ padding-top: 10px;
|
|
|
+ border-top: 1px dashed #e2e7ee
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .lunch-title {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #4a586d;
|
|
|
+ margin-bottom: 8px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .add-shift-btn {
|
|
|
+ width: 100%;
|
|
|
+ border-style: dashed !important;
|
|
|
+ color: #1768e5;
|
|
|
+ font-size: 13px;
|
|
|
+ letter-spacing: 1px
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 考勤设备 Tab 附加样式 ===== */
|
|
|
+.rule-dialog .tab-panel .device-table {
|
|
|
+ margin-top: 10px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .device-table .el-table__header th.el-table__cell {
|
|
|
+ background: #f6f8fb;
|
|
|
+ color: #68778c;
|
|
|
+ font-weight: 600
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .device-table .el-button {
|
|
|
+ padding: 0 4px;
|
|
|
+ font-size: 12px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .status-dot {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 5px;
|
|
|
+ font-size: 12px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .status-dot i {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: inline-block
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .status-dot.on {
|
|
|
+ color: #1ea760
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .status-dot.on i {
|
|
|
+ background: #1ea760;
|
|
|
+ box-shadow: 0 0 0 3px rgba(30, 167, 96, .18);
|
|
|
+ animation: pulse-dot 1.6s ease-in-out infinite
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .status-dot.off {
|
|
|
+ color: #aab4c0
|
|
|
+}
|
|
|
+
|
|
|
+.rule-dialog .tab-panel .status-dot.off i {
|
|
|
+ background: #c0c8d2
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes pulse-dot {
|
|
|
+ 0%, 100% { box-shadow: 0 0 0 3px rgba(30, 167, 96, .18) }
|
|
|
+ 50% { box-shadow: 0 0 0 6px rgba(30, 167, 96, 0) }
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 设备详情 / 日志查看弹窗 ===== */
|
|
|
+.device-view {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #2b3a52
|
|
|
+}
|
|
|
+
|
|
|
+.device-view .view-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 8px 0;
|
|
|
+ border-bottom: 1px dashed #edf0f5
|
|
|
+}
|
|
|
+
|
|
|
+.device-view .view-row:last-child {
|
|
|
+ border-bottom: 0
|
|
|
+}
|
|
|
+
|
|
|
+.device-view .view-row span {
|
|
|
+ flex: none;
|
|
|
+ width: 110px;
|
|
|
+ color: #8390a5;
|
|
|
+ font-size: 12px
|
|
|
+}
|
|
|
+
|
|
|
+.device-view .view-row strong {
|
|
|
+ flex: 1;
|
|
|
+ color: #24344b;
|
|
|
+ font-weight: 500;
|
|
|
+ word-break: break-all
|
|
|
+}
|
|
|
+
|
|
|
+.device-view .device-log-list {
|
|
|
+ max-height: 420px;
|
|
|
+ overflow-y: auto
|
|
|
+}
|
|
|
+
|
|
|
+.device-view .log-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ padding: 8px 0;
|
|
|
+ border-bottom: 1px dashed #edf0f5;
|
|
|
+ font-size: 12px
|
|
|
+}
|
|
|
+
|
|
|
+.device-view .log-item:last-child {
|
|
|
+ border-bottom: 0
|
|
|
+}
|
|
|
+
|
|
|
+.device-view .log-time {
|
|
|
+ flex: none;
|
|
|
+ width: 150px;
|
|
|
+ color: #8995a5;
|
|
|
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace
|
|
|
+}
|
|
|
+
|
|
|
+.device-view .log-tag {
|
|
|
+ flex: none;
|
|
|
+ display: inline-block;
|
|
|
+ padding: 1px 8px;
|
|
|
+ border-radius: 10px;
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 500
|
|
|
+}
|
|
|
+
|
|
|
+.device-view .log-tag.success {
|
|
|
+ color: #1ea760;
|
|
|
+ background: #e8f7ef
|
|
|
+}
|
|
|
+
|
|
|
+.device-view .log-tag.info {
|
|
|
+ color: #1768e5;
|
|
|
+ background: #e6efff
|
|
|
+}
|
|
|
+
|
|
|
+.device-view .log-tag.warn {
|
|
|
+ color: #c67d23;
|
|
|
+ background: #fdf3e3
|
|
|
+}
|
|
|
+
|
|
|
+.device-view .log-tag.error {
|
|
|
+ color: #d55752;
|
|
|
+ background: #fbe9e8
|
|
|
+}
|
|
|
+
|
|
|
+.device-view .log-msg {
|
|
|
+ flex: 1;
|
|
|
+ color: #2b3a52
|
|
|
+}
|
|
|
+</style>
|
|
|
+<style src="../../../styles/views/attendance/index.scss" lang="scss" scoped>
|
|
|
+.rule-page {
|
|
|
+ padding: 18px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-card {
|
|
|
+ border-radius: 8px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-card .filter-bar--rule {
|
|
|
+ grid-template-columns: 1.5fr 1fr 1fr 1fr auto;
|
|
|
+ background: #fff;
|
|
|
+ border-bottom: 1px solid #edf1f6
|
|
|
+}
|
|
|
+
|
|
|
+.rule-card .filter-bar {
|
|
|
+ padding: 14px 18px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-card .table-toolbar {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 14px 18px;
|
|
|
+ border-bottom: 1px solid #edf1f6;
|
|
|
+ background: #fbfcfe
|
|
|
+}
|
|
|
+
|
|
|
+.rule-card .table-toolbar span {
|
|
|
+ color: #6e7d91;
|
|
|
+ font-size: 12px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-card .table-toolbar b {
|
|
|
+ color: #1768e5;
|
|
|
+ font-size: 14px;
|
|
|
+ margin: 0 3px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-table {
|
|
|
+ padding: 0 18px 18px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-table ::v-deep th.el-table__cell {
|
|
|
+ height: 46px;
|
|
|
+ padding: 0 6px;
|
|
|
+ color: #68778c;
|
|
|
+ background: #f6f8fb;
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 12px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-table ::v-deep td.el-table__cell {
|
|
|
+ padding: 8px 6px;
|
|
|
+ border-bottom-color: #edf0f3;
|
|
|
+ font-size: 12px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-table ::v-deep .el-table__row:hover > td.el-table__cell {
|
|
|
+ background: #f2f7ff !important
|
|
|
+}
|
|
|
+
|
|
|
+.rule-page .name-cell strong {
|
|
|
+ display: block;
|
|
|
+ color: #24344b;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 500
|
|
|
+}
|
|
|
+
|
|
|
+.rule-page .name-cell small {
|
|
|
+ display: block;
|
|
|
+ margin-top: 3px;
|
|
|
+ color: #8d99a9;
|
|
|
+ font-size: 10px
|
|
|
+}
|
|
|
+
|
|
|
+.rule-page .method-cell {
|
|
|
+ display: flex;
|
|
|
+ gap: 4px;
|
|
|
+ flex-wrap: wrap
|
|
|
+}
|
|
|
+
|
|
|
+.rule-page .cell-multiline {
|
|
|
+ display: block;
|
|
|
+ color: #6e7d91;
|
|
|
+ font-size: 11px;
|
|
|
+ line-height: 1.5;
|
|
|
+ max-width: 240px
|
|
|
+}
|
|
|
+</style>
|