feat(M08-B): 补保洁员资料管理

This commit is contained in:
Codex
2026-06-30 10:40:45 +08:00
parent e48d1f0301
commit 326c534ba7
16 changed files with 527 additions and 22 deletions
+220
View File
@@ -0,0 +1,220 @@
<template>
<section class="panel">
<div class="panel-toolbar">
<div class="toolbar-actions">
<el-input
v-model="searchDraft"
class="search-input"
placeholder="姓名/手机号/ID"
clearable
@keyup.enter="emitSearch"
/>
<el-segmented
:model-value="status"
:options="statusOptions"
@update:model-value="$emit('status-change', $event as UserStatus | '')"
/>
</div>
<div class="toolbar-actions">
<el-button type="primary" :icon="UserPlus" @click="createDialog.open = true">
新增
</el-button>
<el-tooltip content="刷新保洁员">
<el-button :icon="RefreshCw" :loading="loading" circle @click="$emit('refresh')" />
</el-tooltip>
</div>
</div>
<el-table :data="items" :loading="loading" class="data-table" row-key="id">
<el-table-column prop="nickname" label="保洁员" min-width="160" fixed>
<template #default="{ row }">
<div class="stack">
<strong>{{ row.nickname || compactText(row.id) }}</strong>
<span>ID {{ row.id }} · {{ row.maskedPhone }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="110">
<template #default="{ row }">
<el-tag :type="row.status === 'ACTIVE' ? 'success' : 'info'" effect="light">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="门店范围" min-width="150">
<template #default="{ row }">{{ row.storeIds.join(', ') || '未配置' }}</template>
</el-table-column>
<el-table-column prop="note" label="备注" min-width="180">
<template #default="{ row }">{{ compactText(row.note) }}</template>
</el-table-column>
<el-table-column prop="lastLoginAt" label="最近登录" width="130">
<template #default="{ row }">{{ shortDate(row.lastLoginAt) }}</template>
</el-table-column>
<el-table-column prop="maskedLastIp" label="最近 IP" width="120" />
<el-table-column label="操作" width="310" fixed="right">
<template #default="{ row }">
<div class="row-actions">
<el-button size="small" :icon="Pencil" @click="openEdit(row)">编辑</el-button>
<el-button
size="small"
:type="row.status === 'ACTIVE' ? 'warning' : 'success'"
:icon="row.status === 'ACTIVE' ? UserX : UserCheck"
@click="$emit('status-toggle', { userId: row.id, status: row.status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE' })"
>
{{ row.status === 'ACTIVE' ? '停用' : '启用' }}
</el-button>
<el-button size="small" :icon="ShieldX" @click="$emit('reset-sessions', row.id)">
会话
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
layout="prev, pager, next, total"
:current-page="page"
:page-size="pageSize"
:total="total"
@current-change="$emit('page-change', $event)"
/>
</div>
</section>
<el-dialog v-model="createDialog.open" title="新增保洁员" width="460px">
<el-form label-position="top">
<el-form-item label="姓名">
<el-input v-model="createDialog.nickname" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="createDialog.phone" inputmode="tel" />
</el-form-item>
<el-form-item label="门店 ID,逗号分隔">
<el-input v-model="createDialog.storeIdsText" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="createDialog.note" type="textarea" :rows="3" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="createDialog.open = false">取消</el-button>
<el-button type="primary" @click="submitCreate">创建</el-button>
</template>
</el-dialog>
<el-dialog v-model="editDialog.open" title="编辑保洁员" width="460px">
<el-form label-position="top">
<el-form-item label="姓名">
<el-input v-model="editDialog.nickname" />
</el-form-item>
<el-form-item label="门店 ID,逗号分隔">
<el-input v-model="editDialog.storeIdsText" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="editDialog.note" type="textarea" :rows="3" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editDialog.open = false">取消</el-button>
<el-button type="primary" @click="submitEdit">保存</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive, ref, watch } from 'vue';
import {
Pencil,
RefreshCw,
ShieldX,
UserCheck,
UserPlus,
UserX
} from '@lucide/vue';
import { compactText, shortDate } from '../format';
import type { ManagedUser, PageResult, UserStatus } from '../types';
const props = defineProps<{
loading: boolean;
items: PageResult<ManagedUser>['items'];
total: number;
page: number;
pageSize: number;
status: UserStatus | '';
search: string;
}>();
const emit = defineEmits<{
refresh: [];
'status-change': [UserStatus | ''];
'search-change': [string];
'page-change': [number];
create: [{ nickname: string; phone: string; storeIds: string[]; note?: string }];
update: [{ userId: string; nickname: string; storeIds: string[]; note?: string }];
'status-toggle': [{ userId: string; status: UserStatus }];
'reset-sessions': [string];
}>();
const statusOptions = [
{ label: '全部', value: '' },
{ label: '启用', value: 'ACTIVE' },
{ label: '停用', value: 'DISABLED' }
];
const searchDraft = ref(props.search);
const createDialog = reactive({
open: false,
nickname: '',
phone: '',
storeIdsText: '',
note: ''
});
const editDialog = reactive({
open: false,
userId: '',
nickname: '',
storeIdsText: '',
note: ''
});
watch(
() => props.search,
(value) => { searchDraft.value = value; }
);
function parseStoreIds(value: string) {
return value.split(/[,\s]+/).map((item) => item.trim()).filter(Boolean);
}
function emitSearch() {
emit('search-change', searchDraft.value.trim());
}
function submitCreate() {
emit('create', {
nickname: createDialog.nickname,
phone: createDialog.phone,
storeIds: parseStoreIds(createDialog.storeIdsText),
note: createDialog.note
});
Object.assign(createDialog, { open: false, nickname: '', phone: '', storeIdsText: '', note: '' });
}
function openEdit(row: ManagedUser) {
editDialog.open = true;
editDialog.userId = row.id;
editDialog.nickname = row.nickname;
editDialog.storeIdsText = row.storeIds.join(', ');
editDialog.note = row.note;
}
function submitEdit() {
emit('update', {
userId: editDialog.userId,
nickname: editDialog.nickname,
storeIds: parseStoreIds(editDialog.storeIdsText),
note: editDialog.note
});
editDialog.open = false;
}
</script>