<template>
|
<view class="stock-check-page">
|
<PageHeader title="库存盘点"
|
@back="goBack" />
|
|
<!-- 搜索区域 -->
|
<view class="search-section">
|
<view class="search-row">
|
<view class="search-input">
|
<up-input class="search-text"
|
v-model="searchForm.checkNo"
|
placeholder="盘点单号"
|
clearable
|
@confirm="handleQuery" />
|
</view>
|
<view class="search-input">
|
<up-input class="search-text"
|
v-model="searchForm.checkName"
|
placeholder="盘点名称"
|
clearable
|
@confirm="handleQuery" />
|
</view>
|
</view>
|
<view class="filter-row">
|
<view class="filter-item"
|
@click="showStatusSheet = true">
|
<text class="filter-text">{{ statusText }}</text>
|
<up-icon name="arrow-down"
|
size="14"
|
color="#909399"></up-icon>
|
</view>
|
<view class="filter-item date-item"
|
@click="showCalendar = true">
|
<up-icon name="calendar"
|
size="16"
|
color="#909399"></up-icon>
|
<text class="filter-text"
|
:class="{ placeholder: !searchForm.beginTime }">{{ dateRangeText }}</text>
|
<up-icon v-if="searchForm.beginTime"
|
name="close-circle-fill"
|
size="16"
|
color="#c0c4cc"
|
@click.stop="handleClearDate"></up-icon>
|
</view>
|
<view class="search-btn">
|
<up-button type="primary"
|
size="small"
|
text="搜索"
|
@click="handleQuery"></up-button>
|
</view>
|
</view>
|
</view>
|
|
<!-- 列表区域 -->
|
<scroll-view scroll-y
|
class="list-container"
|
v-if="tableData.length > 0"
|
@scrolltolower="loadMore">
|
<view v-for="item in tableData"
|
:key="item.id"
|
class="ledger-item">
|
<view class="item-header">
|
<view class="item-left">
|
<view class="document-icon">
|
<up-icon name="file-text"
|
size="16"
|
color="#ffffff"></up-icon>
|
</view>
|
<text class="item-id">{{ item.checkNo || '-' }}</text>
|
</view>
|
<view class="item-right">
|
<up-tag :text="getStatusText(item.status)"
|
:type="getStatusType(item.status)"
|
size="mini" />
|
</view>
|
</view>
|
<up-divider></up-divider>
|
<view class="item-details">
|
<view class="detail-row">
|
<text class="detail-label">盘点名称</text>
|
<text class="detail-value">{{ item.checkName || '-' }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">盘点日期</text>
|
<text class="detail-value">{{ formatDate(item.checkDate) }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">盘点人</text>
|
<text class="detail-value">{{ item.checker || '-' }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">盘点种类数</text>
|
<text class="detail-value">{{ Number(item.totalKinds) || 0 }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">差异种类数</text>
|
<text class="detail-value"
|
:class="{ danger: Number(item.diffKinds) > 0 }">{{ Number(item.diffKinds) || 0 }}</text>
|
</view>
|
</view>
|
<view class="action-buttons">
|
<up-button type="info"
|
size="small"
|
plain
|
text="详情"
|
@click="goDetail(item)"></up-button>
|
<up-button v-if="item.status === 0"
|
type="success"
|
size="small"
|
plain
|
text="完成"
|
@click="handleFinish(item)"></up-button>
|
<up-button v-if="item.status === 0"
|
type="error"
|
size="small"
|
plain
|
text="取消"
|
@click="handleCancel(item)"></up-button>
|
</view>
|
</view>
|
<up-loadmore :status="loadStatus" />
|
</scroll-view>
|
<view v-else-if="!loading"
|
class="no-data">
|
<up-empty mode="data"
|
text="暂无盘点数据"></up-empty>
|
</view>
|
|
<!-- 底部操作 -->
|
<view class="footer-btns">
|
<up-button type="primary"
|
text="发起盘点"
|
@click="openAdd"></up-button>
|
</view>
|
|
<!-- 发起盘点弹窗 -->
|
<up-popup :show="showAddPopup"
|
mode="bottom"
|
:round="10"
|
closeable
|
@close="showAddPopup = false">
|
<view class="form-popup">
|
<view class="popup-header">
|
<text class="popup-title">发起盘点</text>
|
</view>
|
<scroll-view class="popup-content"
|
scroll-y>
|
<view class="form-field">
|
<text class="field-label required">盘点名称</text>
|
<up-input v-model="form.checkName"
|
placeholder="请输入盘点名称"
|
maxlength="100" />
|
</view>
|
<view class="form-field">
|
<text class="field-label">盘点日期</text>
|
<view class="pick-field"
|
@click="showDatePicker = true">
|
<up-input class="pick-input"
|
v-model="form.checkDate"
|
readonly
|
placeholder="请选择盘点日期" />
|
<up-icon name="arrow-right"
|
color="#c0c4cc"></up-icon>
|
</view>
|
</view>
|
<view class="form-field">
|
<text class="field-label">盘点人</text>
|
<up-input v-model="form.checker"
|
placeholder="请输入盘点人" />
|
</view>
|
<view class="form-field">
|
<text class="field-label">备注</text>
|
<up-input v-model="form.remark"
|
placeholder="请输入备注"
|
maxlength="200" />
|
</view>
|
<view class="form-field">
|
<text class="field-label">指定产品规格</text>
|
<view class="spec-box">
|
<view v-if="selectedModels.length"
|
class="spec-tags">
|
<view v-for="m in selectedModels"
|
:key="m.id"
|
class="spec-tag"
|
@click="removeModel(m.id)">
|
<text class="spec-tag-text">{{ m.model || m.productName }}</text>
|
<up-icon name="close"
|
size="12"
|
color="#2979ff"></up-icon>
|
</view>
|
</view>
|
<text v-else
|
class="spec-empty">未选择时对全部库存行做快照</text>
|
<up-button type="primary"
|
size="small"
|
plain
|
text="选择规格"
|
@click="openSpecPicker"></up-button>
|
</view>
|
</view>
|
</scroll-view>
|
<view class="popup-footer">
|
<up-button class="btn-cancel"
|
text="取消"
|
@click="showAddPopup = false"></up-button>
|
<up-button class="btn-confirm"
|
type="primary"
|
text="确认"
|
:loading="submitting"
|
@click="handleSubmit"></up-button>
|
</view>
|
</view>
|
</up-popup>
|
|
<!-- 规格选择弹窗 -->
|
<up-popup :show="showSpecPopup"
|
mode="bottom"
|
:round="10"
|
closeable
|
@close="showSpecPopup = false">
|
<view class="form-popup">
|
<view class="popup-header">
|
<text class="popup-title">选择产品规格</text>
|
</view>
|
<view class="spec-category">
|
<scroll-view scroll-x
|
class="category-scroll">
|
<view class="category-list">
|
<view v-for="p in productOptions"
|
:key="p.id"
|
class="category-tag"
|
:class="{ active: p.id === activeProductId }"
|
@click="selectProduct(p)">
|
<text>{{ p.productName }}</text>
|
</view>
|
</view>
|
</scroll-view>
|
</view>
|
<scroll-view class="popup-content"
|
scroll-y>
|
<view v-if="modelOptions.length">
|
<view v-for="m in modelOptions"
|
:key="m.id"
|
class="model-item"
|
@click="toggleModel(m)">
|
<view class="model-info">
|
<text class="model-name">{{ m.model || '-' }}</text>
|
<text class="model-unit">{{ m.unit || '' }}</text>
|
</view>
|
<up-icon :name="isSelected(m.id) ? 'checkmark-circle-fill' : 'circle'"
|
size="22"
|
:color="isSelected(m.id) ? '#2979ff' : '#c0c4cc'"></up-icon>
|
</view>
|
</view>
|
<view v-else
|
class="spec-empty-box">
|
<up-empty mode="data"
|
text="暂无规格数据"></up-empty>
|
</view>
|
</scroll-view>
|
<view class="popup-footer">
|
<up-button class="btn-cancel"
|
text="清空"
|
@click="selectedModels = []"></up-button>
|
<up-button class="btn-confirm"
|
type="primary"
|
text="确定"
|
@click="showSpecPopup = false"></up-button>
|
</view>
|
</view>
|
</up-popup>
|
|
<!-- 状态筛选 -->
|
<up-action-sheet :show="showStatusSheet"
|
:actions="statusActions"
|
title="盘点状态"
|
@select="onStatusSelect"
|
@close="showStatusSheet = false"></up-action-sheet>
|
|
<!-- 日期区间 -->
|
<up-calendar :show="showCalendar"
|
mode="range"
|
:maxDate="maxDate"
|
minDate="2022-01-01"
|
:monthNum="monthNum"
|
@confirm="onDateConfirm"
|
@close="showCalendar = false"></up-calendar>
|
|
<!-- 盘点日期 -->
|
<up-popup :show="showDatePicker"
|
mode="bottom"
|
@close="showDatePicker = false">
|
<up-datetime-picker :show="true"
|
v-model="dateValue"
|
mode="date"
|
@confirm="onCheckDateConfirm"
|
@cancel="showDatePicker = false" />
|
</up-popup>
|
</view>
|
</template>
|
|
<script setup>
|
import { ref, reactive, computed } from "vue";
|
import { onShow } from "@dcloudio/uni-app";
|
import dayjs from "dayjs";
|
import PageHeader from "@/components/PageHeader.vue";
|
import { toast } from "@/utils/common";
|
import { productTreeList, modelList } from "@/api/basicData/product.js";
|
import {
|
start,
|
listPage,
|
finish,
|
cancel,
|
} from "@/api/inventoryManagement/stockCheck.js";
|
|
const loading = ref(false);
|
const submitting = ref(false);
|
const loadStatus = ref("loadmore");
|
const tableData = ref([]);
|
const page = reactive({ current: 1, size: 10, total: 0 });
|
|
const searchForm = reactive({
|
checkNo: "",
|
checkName: "",
|
status: null,
|
beginTime: "",
|
endTime: "",
|
});
|
|
const maxDate = dayjs().format("YYYY-MM-DD");
|
const monthNum = computed(() => {
|
const min = dayjs("2022-01-01");
|
const max = dayjs(maxDate);
|
return max.diff(min, "month") + 1;
|
});
|
const showCalendar = ref(false);
|
const showStatusSheet = ref(false);
|
const showDatePicker = ref(false);
|
const dateValue = ref(new Date().getTime());
|
|
const statusActions = ref([
|
{ name: "全部", value: null },
|
{ name: "盘点中", value: 0 },
|
{ name: "已完成", value: 1 },
|
{ name: "已取消", value: 2 },
|
]);
|
|
const statusText = computed(() => {
|
const hit = statusActions.value.find(a => a.value === searchForm.status);
|
return hit ? hit.name : "全部";
|
});
|
|
const dateRangeText = computed(() => {
|
if (searchForm.beginTime && searchForm.endTime) {
|
return `${searchForm.beginTime.slice(0, 10)} 至 ${searchForm.endTime.slice(0, 10)}`;
|
}
|
return "选择日期区间";
|
});
|
|
// 发起盘点表单
|
const showAddPopup = ref(false);
|
const form = reactive({
|
checkName: "",
|
checkDate: dayjs().format("YYYY-MM-DD"),
|
checker: "",
|
remark: "",
|
});
|
|
// 规格选择
|
const showSpecPopup = ref(false);
|
const productOptions = ref([]);
|
const modelOptions = ref([]);
|
const activeProductId = ref(null);
|
const selectedModels = ref([]);
|
|
const goBack = () => {
|
uni.navigateBack();
|
};
|
|
const formatDate = val => (val ? dayjs(val).format("YYYY-MM-DD") : "-");
|
|
const getStatusText = status => {
|
const map = { 0: "盘点中", 1: "已完成", 2: "已取消" };
|
return map[status] || "未知";
|
};
|
|
const getStatusType = status => {
|
const map = { 0: "warning", 1: "success", 2: "info" };
|
return map[status] || "info";
|
};
|
|
const getList = () => {
|
loading.value = true;
|
loadStatus.value = "loading";
|
listPage({
|
current: page.current,
|
size: page.size,
|
checkNo: searchForm.checkNo,
|
checkName: searchForm.checkName,
|
status: searchForm.status,
|
beginTime: searchForm.beginTime,
|
endTime: searchForm.endTime,
|
})
|
.then(res => {
|
const records = res?.data?.records || [];
|
tableData.value =
|
page.current === 1 ? records : [...tableData.value, ...records];
|
page.total = res?.data?.total || 0;
|
loadStatus.value =
|
tableData.value.length >= page.total ? "nomore" : "loadmore";
|
})
|
.catch(() => {
|
loadStatus.value = "loadmore";
|
})
|
.finally(() => {
|
loading.value = false;
|
});
|
};
|
|
const handleQuery = () => {
|
page.current = 1;
|
tableData.value = [];
|
getList();
|
};
|
|
const loadMore = () => {
|
if (loadStatus.value === "nomore" || loading.value) return;
|
page.current++;
|
getList();
|
};
|
|
const onStatusSelect = item => {
|
searchForm.status = item.value;
|
showStatusSheet.value = false;
|
handleQuery();
|
};
|
|
const onDateConfirm = e => {
|
const list = Array.isArray(e) ? e : [e];
|
searchForm.beginTime = `${list[0]} 00:00:00`;
|
searchForm.endTime = `${list[list.length - 1]} 23:59:59`;
|
showCalendar.value = false;
|
handleQuery();
|
};
|
|
const handleClearDate = () => {
|
searchForm.beginTime = "";
|
searchForm.endTime = "";
|
handleQuery();
|
};
|
|
const onCheckDateConfirm = e => {
|
form.checkDate = dayjs(e.value).format("YYYY-MM-DD");
|
showDatePicker.value = false;
|
};
|
|
const openAdd = () => {
|
form.checkName = "";
|
form.checkDate = dayjs().format("YYYY-MM-DD");
|
form.checker = "";
|
form.remark = "";
|
selectedModels.value = [];
|
showAddPopup.value = true;
|
};
|
|
const openSpecPicker = async () => {
|
showSpecPopup.value = true;
|
if (productOptions.value.length) return;
|
try {
|
const res = await productTreeList();
|
const list = Array.isArray(res) ? res : res?.data || [];
|
productOptions.value = list
|
.filter(item => item.parentId === null)
|
.map(item => ({ id: item.id, productName: item.productName }));
|
if (productOptions.value.length) {
|
selectProduct(productOptions.value[0]);
|
}
|
} catch (e) {
|
// 请求失败已由 request 统一提示
|
}
|
};
|
|
const selectProduct = async p => {
|
activeProductId.value = p.id;
|
modelOptions.value = [];
|
try {
|
const res = await modelList({ id: p.id });
|
const list = Array.isArray(res) ? res : res?.data || [];
|
modelOptions.value = list.map(m => ({
|
id: m.id,
|
model: m.model,
|
unit: m.unit,
|
productName: p.productName,
|
}));
|
} catch (e) {
|
// 请求失败已由 request 统一提示
|
}
|
};
|
|
const isSelected = id => selectedModels.value.some(m => m.id === id);
|
|
const toggleModel = m => {
|
const index = selectedModels.value.findIndex(item => item.id === m.id);
|
if (index > -1) {
|
selectedModels.value.splice(index, 1);
|
} else {
|
selectedModels.value.push(m);
|
}
|
};
|
|
const removeModel = id => {
|
const index = selectedModels.value.findIndex(item => item.id === id);
|
if (index > -1) selectedModels.value.splice(index, 1);
|
};
|
|
const handleSubmit = async () => {
|
if (!form.checkName || !form.checkName.trim()) {
|
toast("请输入盘点名称");
|
return;
|
}
|
submitting.value = true;
|
uni.showLoading({ title: "提交中...", mask: true });
|
try {
|
await start({
|
checkName: form.checkName.trim(),
|
checkDate: form.checkDate,
|
checker: form.checker,
|
remark: form.remark,
|
productModelIds: selectedModels.value.map(m => m.id),
|
});
|
uni.hideLoading();
|
uni.showToast({ title: "发起成功", icon: "success" });
|
showAddPopup.value = false;
|
handleQuery();
|
} catch (e) {
|
uni.hideLoading();
|
} finally {
|
submitting.value = false;
|
}
|
};
|
|
const goDetail = item => {
|
uni.navigateTo({
|
url: `/pages/inventoryManagement/stockCheck/detail?id=${item.id}`,
|
});
|
};
|
|
const handleFinish = item => {
|
uni.showModal({
|
title: "完成盘点",
|
content: "差异将生成调账单据,审核通过后账面生效,是否继续?",
|
success: async res => {
|
if (!res.confirm) return;
|
uni.showLoading({ title: "处理中...", mask: true });
|
try {
|
const r = await finish({ id: item.id });
|
uni.hideLoading();
|
uni.showToast({ title: r?.msg || "盘点已完成", icon: "none" });
|
handleQuery();
|
} catch (e) {
|
uni.hideLoading();
|
}
|
},
|
});
|
};
|
|
const handleCancel = item => {
|
uni.showModal({
|
title: "取消盘点",
|
content: "取消后该盘点单不可再录入,是否继续?",
|
success: async res => {
|
if (!res.confirm) return;
|
uni.showLoading({ title: "处理中...", mask: true });
|
try {
|
const r = await cancel({ id: item.id });
|
uni.hideLoading();
|
uni.showToast({ title: r?.msg || "已取消", icon: "none" });
|
handleQuery();
|
} catch (e) {
|
uni.hideLoading();
|
}
|
},
|
});
|
};
|
|
onShow(() => {
|
handleQuery();
|
});
|
</script>
|
|
<style scoped lang="scss">
|
@import "@/styles/procurement-common.scss";
|
|
.stock-check-page {
|
min-height: 100vh;
|
background: #f8f9fa;
|
display: flex;
|
flex-direction: column;
|
}
|
|
.search-section {
|
background: #fff;
|
padding: 10px 12px;
|
}
|
|
.search-row {
|
display: flex;
|
gap: 10px;
|
margin-bottom: 10px;
|
|
.search-input {
|
flex: 1;
|
background: #f5f5f5;
|
border-radius: 8px;
|
padding: 0 12px;
|
}
|
}
|
|
.filter-row {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
|
.filter-item {
|
display: flex;
|
align-items: center;
|
gap: 4px;
|
background: #f5f5f5;
|
border-radius: 8px;
|
padding: 0 12px;
|
height: 34px;
|
font-size: 13px;
|
color: #333;
|
|
&.date-item {
|
flex: 1;
|
overflow: hidden;
|
}
|
|
.filter-text {
|
font-size: 13px;
|
color: #333;
|
white-space: nowrap;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
|
&.placeholder {
|
color: #999;
|
}
|
}
|
}
|
|
.search-btn {
|
width: 70px;
|
}
|
}
|
|
.list-container {
|
flex: 1;
|
height: 0;
|
padding: 20rpx 20rpx 140rpx;
|
box-sizing: border-box;
|
}
|
|
.detail-value.danger {
|
color: #ee0a24;
|
font-weight: 600;
|
}
|
|
.action-buttons {
|
display: flex;
|
gap: 12px;
|
padding: 16rpx 0 0;
|
justify-content: flex-end;
|
}
|
|
.no-data {
|
padding-top: 150rpx;
|
}
|
|
.form-popup {
|
height: 70vh;
|
display: flex;
|
flex-direction: column;
|
background: #f5f5f5;
|
}
|
|
.popup-content {
|
flex: 1;
|
height: 0;
|
padding: 12px;
|
}
|
|
.form-field {
|
background: #fff;
|
border-radius: 10px;
|
padding: 12px;
|
margin-bottom: 12px;
|
}
|
|
// readonly 的 up-input 会把原生 input 置为 disabled,disabled 元素会吞掉点击事件,
|
// 故让 input 不参与指针事件,由外层 .pick-field 接管点击
|
.pick-field {
|
display: flex;
|
align-items: center;
|
|
.pick-input {
|
flex: 1;
|
}
|
|
:deep(input) {
|
pointer-events: none;
|
}
|
}
|
|
.field-label {
|
display: block;
|
font-size: 14px;
|
color: #666;
|
margin-bottom: 8px;
|
|
&.required::before {
|
content: "*";
|
color: #ff0000;
|
margin-right: 4px;
|
}
|
}
|
|
.spec-box {
|
display: flex;
|
flex-direction: column;
|
gap: 10px;
|
}
|
|
.spec-tags {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 8px;
|
}
|
|
.spec-tag {
|
display: flex;
|
align-items: center;
|
gap: 6px;
|
background: #ecf5ff;
|
color: #2979ff;
|
border-radius: 6px;
|
padding: 4px 10px;
|
|
.spec-tag-text {
|
font-size: 12px;
|
}
|
}
|
|
.spec-empty {
|
font-size: 12px;
|
color: #999;
|
}
|
|
.spec-category {
|
background: #fff;
|
padding: 8px 0;
|
border-bottom: 1px solid #f0f0f0;
|
}
|
|
.category-scroll {
|
white-space: nowrap;
|
}
|
|
.category-list {
|
display: inline-flex;
|
gap: 10px;
|
padding: 0 12px;
|
}
|
|
.category-tag {
|
padding: 6px 14px;
|
border-radius: 20px;
|
background: #f5f5f5;
|
color: #666;
|
font-size: 13px;
|
white-space: nowrap;
|
|
&.active {
|
background: #2979ff;
|
color: #fff;
|
}
|
}
|
|
.model-item {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
background: #fff;
|
border-radius: 10px;
|
padding: 14px 12px;
|
margin-bottom: 8px;
|
|
.model-info {
|
display: flex;
|
flex-direction: column;
|
gap: 4px;
|
}
|
|
.model-name {
|
font-size: 14px;
|
color: #333;
|
}
|
|
.model-unit {
|
font-size: 12px;
|
color: #999;
|
}
|
}
|
|
.spec-empty-box {
|
padding-top: 60px;
|
}
|
|
.popup-footer {
|
display: flex;
|
gap: 12px;
|
padding: 12px 16px;
|
background: #fff;
|
border-top: 1px solid #f0f0f0;
|
}
|
|
.btn-cancel,
|
.btn-confirm {
|
flex: 1;
|
}
|
</style>
|