|
|
@@ -0,0 +1,223 @@
|
|
|
+<script setup lang="ts" name="aidopS8OperatorBindingTab">
|
|
|
+import { computed, onMounted, reactive, ref } from 'vue';
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus';
|
|
|
+import { s8ConfigApi, type S8OperatorBindingRow, type S8ConfigSysUserRow } from '../api/s8ConfigApi';
|
|
|
+
|
|
|
+const filters = reactive({
|
|
|
+ factoryRefId: 1,
|
|
|
+ bindStatus: '' as '' | 'BOUND' | 'UNBOUND',
|
|
|
+ keyword: '',
|
|
|
+});
|
|
|
+
|
|
|
+const loading = ref(false);
|
|
|
+const rows = ref<S8OperatorBindingRow[]>([]);
|
|
|
+
|
|
|
+async function loadRows() {
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ rows.value = (await s8ConfigApi.operatorBindings.list({
|
|
|
+ factoryRefId: filters.factoryRefId || undefined,
|
|
|
+ bindStatus: filters.bindStatus || undefined,
|
|
|
+ keyword: filters.keyword?.trim() || undefined,
|
|
|
+ })) as S8OperatorBindingRow[];
|
|
|
+ } catch {
|
|
|
+ // 拦截器已 toast;这里吞掉避免 Uncaught (in promise)
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const dialogVisible = ref(false);
|
|
|
+const dialogTarget = ref<S8OperatorBindingRow | null>(null);
|
|
|
+const dialogForm = reactive<{ sysUserId: number | null }>({ sysUserId: null });
|
|
|
+const sysUserOptions = ref<S8ConfigSysUserRow[]>([]);
|
|
|
+const sysUserKeyword = ref('');
|
|
|
+const sysUserLoading = ref(false);
|
|
|
+
|
|
|
+async function loadSysUsers() {
|
|
|
+ if (!dialogTarget.value) return;
|
|
|
+ sysUserLoading.value = true;
|
|
|
+ try {
|
|
|
+ sysUserOptions.value = (await s8ConfigApi.operatorBindings.sysUsers({
|
|
|
+ keyword: sysUserKeyword.value?.trim() || undefined,
|
|
|
+ excludeEmployeeId: dialogTarget.value.employeeId,
|
|
|
+ })) as S8ConfigSysUserRow[];
|
|
|
+ } catch {
|
|
|
+ // silent — interceptor toast already shown
|
|
|
+ } finally {
|
|
|
+ sysUserLoading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function openBindDialog(row: S8OperatorBindingRow) {
|
|
|
+ dialogTarget.value = row;
|
|
|
+ dialogForm.sysUserId = row.sysUserId ?? null;
|
|
|
+ sysUserKeyword.value = '';
|
|
|
+ sysUserOptions.value = [];
|
|
|
+ dialogVisible.value = true;
|
|
|
+ loadSysUsers();
|
|
|
+}
|
|
|
+
|
|
|
+function isOptionDisabled(opt: S8ConfigSysUserRow): boolean {
|
|
|
+ if (!dialogTarget.value) return false;
|
|
|
+ if (opt.alreadyBoundEmployeeId == null) return false;
|
|
|
+ return opt.alreadyBoundEmployeeId !== dialogTarget.value.employeeId;
|
|
|
+}
|
|
|
+
|
|
|
+async function submitBind() {
|
|
|
+ if (!dialogTarget.value) return;
|
|
|
+ if (!dialogForm.sysUserId) {
|
|
|
+ ElMessage.warning('请选择系统账号');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ await s8ConfigApi.operatorBindings.bind({
|
|
|
+ employeeId: dialogTarget.value.employeeId,
|
|
|
+ sysUserId: dialogForm.sysUserId,
|
|
|
+ factoryRefId: dialogTarget.value.factoryRefId,
|
|
|
+ });
|
|
|
+ ElMessage.success('绑定成功');
|
|
|
+ dialogVisible.value = false;
|
|
|
+ await loadRows();
|
|
|
+ } catch {
|
|
|
+ // 拦截器已弹出业务 4xx 文案
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function confirmUnbind(row: S8OperatorBindingRow) {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(
|
|
|
+ `解绑后员工 ${row.name || row.empCode} 不能承担处理人/检验人,直到重新绑定。是否继续?`,
|
|
|
+ '解绑确认',
|
|
|
+ { type: 'warning', confirmButtonText: '解绑', cancelButtonText: '取消' },
|
|
|
+ );
|
|
|
+ } catch {
|
|
|
+ return; // 用户取消
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ await s8ConfigApi.operatorBindings.unbind(row.employeeId);
|
|
|
+ ElMessage.success('已解绑');
|
|
|
+ await loadRows();
|
|
|
+ } catch {
|
|
|
+ // 拦截器已 toast
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const stats = computed(() => {
|
|
|
+ const total = rows.value.length;
|
|
|
+ const bound = rows.value.filter((r) => r.bindStatus === 'BOUND').length;
|
|
|
+ return { total, bound, unbound: total - bound };
|
|
|
+});
|
|
|
+
|
|
|
+onMounted(loadRows);
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="operator-binding-tab">
|
|
|
+ <el-form :inline="true" class="filter-bar">
|
|
|
+ <el-form-item label="工厂 ID">
|
|
|
+ <el-input-number v-model="filters.factoryRefId" :min="1" :step="1" :controls="false" style="width: 120px" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="绑定状态">
|
|
|
+ <el-select v-model="filters.bindStatus" placeholder="全部" clearable style="width: 140px">
|
|
|
+ <el-option label="全部" value="" />
|
|
|
+ <el-option label="已绑定" value="BOUND" />
|
|
|
+ <el-option label="未绑定" value="UNBOUND" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="工号 / 姓名">
|
|
|
+ <el-input v-model="filters.keyword" placeholder="empCode / name" clearable style="width: 200px" @keyup.enter="loadRows" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" @click="loadRows">查询</el-button>
|
|
|
+ <el-button @click="filters.bindStatus = ''; filters.keyword = ''; loadRows()">重置</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <div class="stats-bar">
|
|
|
+ <el-tag>共 {{ stats.total }}</el-tag>
|
|
|
+ <el-tag type="success">已绑定 {{ stats.bound }}</el-tag>
|
|
|
+ <el-tag type="warning">未绑定 {{ stats.unbound }}</el-tag>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-table v-loading="loading" :data="rows" stripe size="small" style="width: 100%">
|
|
|
+ <el-table-column prop="empCode" label="工号" width="160" />
|
|
|
+ <el-table-column prop="name" label="姓名" width="160" />
|
|
|
+ <el-table-column label="绑定账号" min-width="200">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span v-if="row.bindStatus === 'BOUND'">{{ row.sysUserName }} <span class="muted">({{ row.sysUserId }})</span></span>
|
|
|
+ <span v-else class="muted">—</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="状态" width="120">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag v-if="row.bindStatus === 'BOUND'" type="success">已绑定</el-tag>
|
|
|
+ <el-tag v-else type="warning">未绑定</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="200">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button v-if="row.bindStatus === 'UNBOUND'" type="primary" size="small" @click="openBindDialog(row)">绑定</el-button>
|
|
|
+ <template v-else>
|
|
|
+ <el-button size="small" @click="openBindDialog(row)">换绑</el-button>
|
|
|
+ <el-button type="danger" size="small" @click="confirmUnbind(row)">解绑</el-button>
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <el-dialog v-model="dialogVisible" :title="`绑定系统账号 - ${dialogTarget?.name ?? ''}`" width="520px">
|
|
|
+ <el-form label-width="100px">
|
|
|
+ <el-form-item label="员工">
|
|
|
+ <span>{{ dialogTarget?.empCode }} / {{ dialogTarget?.name }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="系统账号" required>
|
|
|
+ <el-select
|
|
|
+ v-model="dialogForm.sysUserId"
|
|
|
+ filterable
|
|
|
+ remote
|
|
|
+ :remote-method="(q: string) => { sysUserKeyword = q; loadSysUsers(); }"
|
|
|
+ :loading="sysUserLoading"
|
|
|
+ placeholder="搜索 account / realName"
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="opt in sysUserOptions"
|
|
|
+ :key="opt.id"
|
|
|
+ :value="opt.id"
|
|
|
+ :label="`${opt.realName ?? ''}(${opt.account ?? ''})`"
|
|
|
+ :disabled="isOptionDisabled(opt)"
|
|
|
+ >
|
|
|
+ <div style="display:flex;justify-content:space-between;align-items:center">
|
|
|
+ <span>{{ opt.realName }} <span class="muted">({{ opt.account }})</span></span>
|
|
|
+ <el-tag v-if="isOptionDisabled(opt)" size="small" type="info">已被绑定</el-tag>
|
|
|
+ </div>
|
|
|
+ </el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="dialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="submitBind">确定</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.operator-binding-tab {
|
|
|
+ padding: 8px 0;
|
|
|
+}
|
|
|
+.filter-bar {
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+.stats-bar {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+.muted {
|
|
|
+ color: #999;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+</style>
|