243 lines
7.9 KiB
Vue
243 lines
7.9 KiB
Vue
<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>
|