|
|
@@ -0,0 +1,497 @@
|
|
|
+import dayjs from "dayjs";
|
|
|
+import { message } from "@/utils/message";
|
|
|
+import { addDialog } from "@/components/ReDialog";
|
|
|
+import { usePublicHooks } from "@/views/system/hooks";
|
|
|
+import type { PaginationProps } from "@pureadmin/table";
|
|
|
+import { deviceDetection } from "@pureadmin/utils";
|
|
|
+import {
|
|
|
+ getReplenisherList,
|
|
|
+ createReplenisher,
|
|
|
+ updateReplenisher,
|
|
|
+ updateReplenisherStatus,
|
|
|
+ deleteReplenisher,
|
|
|
+ getBoundDevices,
|
|
|
+ bindDevices,
|
|
|
+ unbindDevice
|
|
|
+} from "@/api/replenisher";
|
|
|
+import { getEnabledShops, getShopDevices } from "@/api/shop";
|
|
|
+import {
|
|
|
+ type Ref,
|
|
|
+ h,
|
|
|
+ ref,
|
|
|
+ toRaw,
|
|
|
+ reactive,
|
|
|
+ onMounted
|
|
|
+} from "vue";
|
|
|
+import {
|
|
|
+ ElForm,
|
|
|
+ ElInput,
|
|
|
+ ElFormItem,
|
|
|
+ ElSelect,
|
|
|
+ ElOption,
|
|
|
+ ElRadioGroup,
|
|
|
+ ElRadioButton,
|
|
|
+ ElButton,
|
|
|
+ ElTag,
|
|
|
+ ElMessageBox,
|
|
|
+ ElPopconfirm
|
|
|
+} from "element-plus";
|
|
|
+import type { ReplenisherFormItem, ReplenisherSearchForm, ShopWithDevices } from "./types";
|
|
|
+
|
|
|
+export function useReplenisher(tableRef: Ref) {
|
|
|
+ const form = reactive<ReplenisherSearchForm>({
|
|
|
+ keyword: "",
|
|
|
+ status: ""
|
|
|
+ });
|
|
|
+ const dialogForm = reactive<any>({
|
|
|
+ id: undefined,
|
|
|
+ name: "",
|
|
|
+ phone: "",
|
|
|
+ employeeId: "",
|
|
|
+ status: 1
|
|
|
+ });
|
|
|
+ const formRef = ref();
|
|
|
+ const ruleFormRef = ref();
|
|
|
+ const dataList = ref([]);
|
|
|
+ const loading = ref(true);
|
|
|
+ const switchLoadMap = ref({});
|
|
|
+ const { switchStyle } = usePublicHooks();
|
|
|
+ const pagination = reactive<PaginationProps>({
|
|
|
+ total: 0,
|
|
|
+ pageSize: 10,
|
|
|
+ currentPage: 1,
|
|
|
+ background: true
|
|
|
+ });
|
|
|
+
|
|
|
+ const columns: TableColumnList = [
|
|
|
+ {
|
|
|
+ label: "补货员ID",
|
|
|
+ prop: "id",
|
|
|
+ width: 90
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "姓名",
|
|
|
+ prop: "name",
|
|
|
+ minWidth: 100
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "手机号",
|
|
|
+ prop: "phone",
|
|
|
+ minWidth: 130
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "工号",
|
|
|
+ prop: "employeeId",
|
|
|
+ minWidth: 100
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "绑定设备数",
|
|
|
+ prop: "boundDeviceCount",
|
|
|
+ width: 110
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "状态",
|
|
|
+ prop: "status",
|
|
|
+ minWidth: 100,
|
|
|
+ cellRenderer: scope => (
|
|
|
+ <el-switch
|
|
|
+ size={scope.props.size === "small" ? "small" : "default"}
|
|
|
+ loading={switchLoadMap.value[scope.index]?.loading}
|
|
|
+ v-model={scope.row.status}
|
|
|
+ active-value={1}
|
|
|
+ inactive-value={0}
|
|
|
+ active-text="正常"
|
|
|
+ inactive-text="禁用"
|
|
|
+ inline-prompt
|
|
|
+ style={switchStyle.value}
|
|
|
+ onChange={() => onChange(scope as any)}
|
|
|
+ />
|
|
|
+ )
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "累计任务",
|
|
|
+ prop: "totalTasks",
|
|
|
+ width: 100
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "最后任务时间",
|
|
|
+ prop: "lastTaskTime",
|
|
|
+ minWidth: 160,
|
|
|
+ formatter: ({ lastTaskTime }) =>
|
|
|
+ lastTaskTime ? dayjs(lastTaskTime).format("YYYY-MM-DD HH:mm:ss") : "-"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "创建时间",
|
|
|
+ prop: "createTime",
|
|
|
+ minWidth: 160,
|
|
|
+ formatter: ({ createTime }) =>
|
|
|
+ createTime ? dayjs(createTime).format("YYYY-MM-DD HH:mm:ss") : "-"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "操作",
|
|
|
+ fixed: "right",
|
|
|
+ width: 220,
|
|
|
+ slot: "operation"
|
|
|
+ }
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 状态切换
|
|
|
+ function onChange({ row, index }) {
|
|
|
+ ElMessageBox.confirm(
|
|
|
+ `确认要<strong>${row.status === 0 ? "禁用" : "启用"}</strong>补货员<strong style='color:var(--el-color-primary)'>${row.name}</strong>吗?`,
|
|
|
+ "系统提示",
|
|
|
+ {
|
|
|
+ confirmButtonText: "确定",
|
|
|
+ cancelButtonText: "取消",
|
|
|
+ type: "warning",
|
|
|
+ dangerouslyUseHTMLString: true,
|
|
|
+ draggable: true
|
|
|
+ }
|
|
|
+ )
|
|
|
+ .then(async () => {
|
|
|
+ switchLoadMap.value[index] = Object.assign({}, switchLoadMap.value[index], {
|
|
|
+ loading: true
|
|
|
+ });
|
|
|
+ try {
|
|
|
+ const res = await updateReplenisherStatus(row.id, { status: row.status });
|
|
|
+ if (res.code === 200) {
|
|
|
+ message(`已${row.status === 0 ? "禁用" : "启用"}补货员 ${row.name}`, {
|
|
|
+ type: "success"
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ row.status = row.status === 0 ? 1 : 0;
|
|
|
+ message(res.message || "操作失败", { type: "error" });
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ row.status = row.status === 0 ? 1 : 0;
|
|
|
+ message("操作失败", { type: "error" });
|
|
|
+ } finally {
|
|
|
+ setTimeout(() => {
|
|
|
+ switchLoadMap.value[index] = Object.assign({}, switchLoadMap.value[index], {
|
|
|
+ loading: false
|
|
|
+ });
|
|
|
+ }, 300);
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ row.status = row.status === 0 ? 1 : 0;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 删除
|
|
|
+ async function handleDelete(row: ReplenisherFormItem) {
|
|
|
+ try {
|
|
|
+ const res = await deleteReplenisher(row.id);
|
|
|
+ if (res.code === 200) {
|
|
|
+ message(`已成功删除补货员 ${row.name}`, { type: "success" });
|
|
|
+ onSearch();
|
|
|
+ } else {
|
|
|
+ message(res.message || "删除失败", { type: "error" });
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ message("删除失败", { type: "error" });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设备绑定管理(门店设备树)
|
|
|
+ const bindDialogVisible = ref(false);
|
|
|
+ const currentReplenisher = ref<ReplenisherFormItem>(null);
|
|
|
+ const deviceLoading = ref(false);
|
|
|
+ const shopDeviceTree = ref<ShopWithDevices[]>([]);
|
|
|
+ const loadingShopTree = ref(false);
|
|
|
+ const selectedDeviceIds = ref<Set<string>>(new Set());
|
|
|
+ const origBoundDeviceIds = ref<Set<string>>(new Set());
|
|
|
+
|
|
|
+ async function openBindDialog(row: ReplenisherFormItem) {
|
|
|
+ currentReplenisher.value = row;
|
|
|
+ bindDialogVisible.value = true;
|
|
|
+ loadingShopTree.value = true;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 并行获取所有启用的门店 + 已绑定的设备
|
|
|
+ const [shopsRes, boundRes] = await Promise.all([
|
|
|
+ getEnabledShops(),
|
|
|
+ getBoundDevices(row.id)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ const shops: any[] = shopsRes.data || [];
|
|
|
+ const boundDevices: string[] = boundRes.data || [];
|
|
|
+ selectedDeviceIds.value = new Set(boundDevices);
|
|
|
+ origBoundDeviceIds.value = new Set(boundDevices);
|
|
|
+
|
|
|
+ // 并行获取每个门店的设备
|
|
|
+ const shopPromises = shops.map(async (shop: any) => {
|
|
|
+ try {
|
|
|
+ const devRes = await getShopDevices(shop.id);
|
|
|
+ const devices: any[] = devRes.data || [];
|
|
|
+ return {
|
|
|
+ id: shop.id,
|
|
|
+ name: shop.name,
|
|
|
+ address: shop.address || "",
|
|
|
+ devices: devices.map((d: any) => ({
|
|
|
+ deviceId: d.deviceId,
|
|
|
+ name: d.name
|
|
|
+ })),
|
|
|
+ expanded: false,
|
|
|
+ loading: false
|
|
|
+ } as ShopWithDevices;
|
|
|
+ } catch {
|
|
|
+ return {
|
|
|
+ id: shop.id,
|
|
|
+ name: shop.name,
|
|
|
+ address: shop.address || "",
|
|
|
+ devices: [],
|
|
|
+ expanded: false,
|
|
|
+ loading: false
|
|
|
+ } as ShopWithDevices;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ shopDeviceTree.value = await Promise.all(shopPromises);
|
|
|
+ } catch (error) {
|
|
|
+ console.error("加载门店设备数据失败:", error);
|
|
|
+ message("加载数据失败", { type: "error" });
|
|
|
+ } finally {
|
|
|
+ loadingShopTree.value = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function toggleDeviceSelect(deviceId: string) {
|
|
|
+ const newSet = new Set(selectedDeviceIds.value);
|
|
|
+ if (newSet.has(deviceId)) {
|
|
|
+ newSet.delete(deviceId);
|
|
|
+ } else {
|
|
|
+ newSet.add(deviceId);
|
|
|
+ }
|
|
|
+ selectedDeviceIds.value = newSet;
|
|
|
+ }
|
|
|
+
|
|
|
+ function toggleShopSelectAll(shop: ShopWithDevices) {
|
|
|
+ const newSet = new Set(selectedDeviceIds.value);
|
|
|
+ const allSelected = shop.devices.every(d => newSet.has(d.deviceId));
|
|
|
+ if (allSelected) {
|
|
|
+ shop.devices.forEach(d => newSet.delete(d.deviceId));
|
|
|
+ } else {
|
|
|
+ shop.devices.forEach(d => newSet.add(d.deviceId));
|
|
|
+ }
|
|
|
+ selectedDeviceIds.value = newSet;
|
|
|
+ }
|
|
|
+
|
|
|
+ function getShopSelectState(shop: ShopWithDevices): "all" | "partial" | "none" {
|
|
|
+ if (shop.devices.length === 0) return "none";
|
|
|
+ const selected = shop.devices.filter(d => selectedDeviceIds.value.has(d.deviceId)).length;
|
|
|
+ if (selected === shop.devices.length) return "all";
|
|
|
+ if (selected > 0) return "partial";
|
|
|
+ return "none";
|
|
|
+ }
|
|
|
+
|
|
|
+ async function handleConfirmBind() {
|
|
|
+ if (!currentReplenisher.value) return;
|
|
|
+
|
|
|
+ const origSet = origBoundDeviceIds.value;
|
|
|
+ const newSet = selectedDeviceIds.value;
|
|
|
+
|
|
|
+ const toBind = [...newSet].filter(id => !origSet.has(id));
|
|
|
+ const toUnbind = [...origSet].filter(id => !newSet.has(id));
|
|
|
+
|
|
|
+ if (toBind.length === 0 && toUnbind.length === 0) {
|
|
|
+ message("未做任何变更", { type: "info" });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ deviceLoading.value = true;
|
|
|
+ try {
|
|
|
+ if (toBind.length > 0) {
|
|
|
+ const res = await bindDevices(currentReplenisher.value.id, { deviceIds: toBind });
|
|
|
+ if (res.code !== 200) {
|
|
|
+ message("绑定设备失败: " + (res.message || ""), { type: "error" });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (toUnbind.length > 0) {
|
|
|
+ await Promise.all(toUnbind.map(deviceId =>
|
|
|
+ unbindDevice(currentReplenisher.value.id, deviceId)
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ message(`设备绑定更新完成(绑定${toBind.length}台,解绑${toUnbind.length}台)`, { type: "success" });
|
|
|
+ bindDialogVisible.value = false;
|
|
|
+ onSearch();
|
|
|
+ } catch (error) {
|
|
|
+ message("操作失败", { type: "error" });
|
|
|
+ } finally {
|
|
|
+ deviceLoading.value = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 分页
|
|
|
+ function handleSizeChange(val: number) {
|
|
|
+ pagination.pageSize = val;
|
|
|
+ onSearch();
|
|
|
+ }
|
|
|
+
|
|
|
+ function handleCurrentChange(val: number) {
|
|
|
+ pagination.currentPage = val;
|
|
|
+ onSearch();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 搜索
|
|
|
+ async function onSearch() {
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ const searchParams: any = {
|
|
|
+ page: pagination.currentPage,
|
|
|
+ pageSize: pagination.pageSize
|
|
|
+ };
|
|
|
+ if (form.keyword) searchParams.keyword = form.keyword;
|
|
|
+ if (form.status) searchParams.status = form.status;
|
|
|
+
|
|
|
+ const { data } = await getReplenisherList(searchParams);
|
|
|
+ if (data) {
|
|
|
+ dataList.value = data.list || [];
|
|
|
+ pagination.total = Number(data.total) || 0;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("获取补货员列表失败:", error);
|
|
|
+ } finally {
|
|
|
+ setTimeout(() => {
|
|
|
+ loading.value = false;
|
|
|
+ }, 300);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重置表单
|
|
|
+ const resetForm = formEl => {
|
|
|
+ if (!formEl) return;
|
|
|
+ formEl.resetFields();
|
|
|
+ pagination.currentPage = 1;
|
|
|
+ onSearch();
|
|
|
+ };
|
|
|
+
|
|
|
+ // 打开弹窗
|
|
|
+ function openDialog(title = "新增", row?: ReplenisherFormItem) {
|
|
|
+ // 初始化对话框表单数据
|
|
|
+ dialogForm.id = row?.id;
|
|
|
+ dialogForm.name = row?.name ?? "";
|
|
|
+ dialogForm.phone = row?.phone ?? "";
|
|
|
+ dialogForm.employeeId = row?.employeeId ?? "";
|
|
|
+ dialogForm.status = row?.status ?? 1;
|
|
|
+ addDialog({
|
|
|
+ title: `${title}补货员`,
|
|
|
+ props: {
|
|
|
+ formInline: {
|
|
|
+ id: row?.id,
|
|
|
+ name: row?.name ?? "",
|
|
|
+ phone: row?.phone ?? "",
|
|
|
+ employeeId: row?.employeeId ?? "",
|
|
|
+ status: row?.status ?? 1
|
|
|
+ }
|
|
|
+ },
|
|
|
+ width: "40%",
|
|
|
+ draggable: true,
|
|
|
+ fullscreen: deviceDetection(),
|
|
|
+ fullscreenIcon: true,
|
|
|
+ closeOnClickModal: false,
|
|
|
+ contentRenderer: () => (
|
|
|
+ <ElForm ref={ruleFormRef} model={dialogForm} label-width="100px">
|
|
|
+ <ElFormItem
|
|
|
+ label="姓名"
|
|
|
+ prop="name"
|
|
|
+ rules={[{ required: true, message: "请输入补货员姓名", trigger: "blur" }]}
|
|
|
+ >
|
|
|
+ <ElInput
|
|
|
+ v-model={dialogForm.name}
|
|
|
+ placeholder="请输入补货员姓名"
|
|
|
+ clearable
|
|
|
+ />
|
|
|
+ </ElFormItem>
|
|
|
+ <ElFormItem
|
|
|
+ label="手机号"
|
|
|
+ prop="phone"
|
|
|
+ >
|
|
|
+ <ElInput v-model={dialogForm.phone} placeholder="请输入手机号" clearable />
|
|
|
+ </ElFormItem>
|
|
|
+ <ElFormItem
|
|
|
+ label="工号"
|
|
|
+ prop="employeeId"
|
|
|
+ >
|
|
|
+ <ElInput v-model={dialogForm.employeeId} placeholder="请输入工号" clearable />
|
|
|
+ </ElFormItem>
|
|
|
+ <ElFormItem label="状态" prop="status">
|
|
|
+ <ElRadioGroup v-model={dialogForm.status}>
|
|
|
+ <ElRadioButton value={1}>正常</ElRadioButton>
|
|
|
+ <ElRadioButton value={0}>禁用</ElRadioButton>
|
|
|
+ </ElRadioGroup>
|
|
|
+ </ElFormItem>
|
|
|
+ </ElForm>
|
|
|
+ ),
|
|
|
+ beforeSure: async (done) => {
|
|
|
+ const valid = await ruleFormRef.value.validate().catch(() => false);
|
|
|
+ if (!valid) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (title === "新增") {
|
|
|
+ const res = await createReplenisher({
|
|
|
+ name: dialogForm.name,
|
|
|
+ phone: dialogForm.phone || undefined,
|
|
|
+ employeeId: dialogForm.employeeId || undefined
|
|
|
+ });
|
|
|
+ if (res.code === 200) {
|
|
|
+ message(`新增补货员 ${dialogForm.name} 成功`, { type: "success" });
|
|
|
+ done();
|
|
|
+ onSearch();
|
|
|
+ } else {
|
|
|
+ message(res.message || "新增失败", { type: "error" });
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const res = await updateReplenisher(row.id, {
|
|
|
+ name: dialogForm.name,
|
|
|
+ phone: dialogForm.phone || undefined,
|
|
|
+ employeeId: dialogForm.employeeId || undefined,
|
|
|
+ status: dialogForm.status
|
|
|
+ });
|
|
|
+ if (res.code === 200) {
|
|
|
+ message(`修改补货员 ${dialogForm.name} 成功`, { type: "success" });
|
|
|
+ done();
|
|
|
+ onSearch();
|
|
|
+ } else {
|
|
|
+ message(res.message || "修改失败", { type: "error" });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ message("操作失败", { type: "error" });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ onMounted(() => {
|
|
|
+ onSearch();
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ form,
|
|
|
+ loading,
|
|
|
+ columns,
|
|
|
+ dataList,
|
|
|
+ pagination,
|
|
|
+ onSearch,
|
|
|
+ resetForm,
|
|
|
+ openDialog,
|
|
|
+ handleDelete,
|
|
|
+ openBindDialog,
|
|
|
+ bindDialogVisible,
|
|
|
+ currentReplenisher,
|
|
|
+ deviceLoading,
|
|
|
+ shopDeviceTree,
|
|
|
+ loadingShopTree,
|
|
|
+ selectedDeviceIds,
|
|
|
+ toggleDeviceSelect,
|
|
|
+ toggleShopSelectAll,
|
|
|
+ getShopSelectState,
|
|
|
+ handleConfirmBind,
|
|
|
+ handleSizeChange,
|
|
|
+ handleCurrentChange
|
|
|
+ };
|
|
|
+}
|