Files
qipai/admin/src/components/CleanersPanel.vue
T
2026-06-30 11:02:43 +08:00

243 lines
7.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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="资料" width="150">
<template #default="{ row }">
<div class="profile-badges">
<el-tag :type="row.wechatMiniappBound ? 'success' : 'warning'" effect="light">
{{ row.wechatMiniappBound ? '已绑微信' : '未绑微信' }}
</el-tag>
<el-tag :type="profileComplete(row) ? 'success' : 'info'" effect="plain">
{{ profileComplete(row) ? '资料完整' : '待补资料' }}
</el-tag>
</div>
</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="手机号">
<el-input v-model="editDialog.phone" inputmode="tel" placeholder="留空则不修改" />
</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; phone?: 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: '',
phone: '',
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.phone = '';
editDialog.storeIdsText = row.storeIds.join(', ');
editDialog.note = row.note;
}
function submitEdit() {
emit('update', {
userId: editDialog.userId,
nickname: editDialog.nickname,
phone: editDialog.phone.trim() || undefined,
storeIds: parseStoreIds(editDialog.storeIdsText),
note: editDialog.note
});
editDialog.open = false;
}
function profileComplete(row: ManagedUser) {
return row.wechatMiniappBound && row.storeIds.length > 0 && Boolean(row.nickname);
}
</script>