<template>
|
<view class="sales-account">
|
<PageHeader title="计件单价配置"
|
@back="goBack" />
|
|
<!-- 搜索区域 -->
|
<view class="search-section">
|
<view class="filter-row">
|
<up-input class="filter-input"
|
v-model="queryParams.operationName"
|
placeholder="工序名称"
|
clearable
|
@change="handleSearch" />
|
<up-input class="filter-input"
|
v-model="queryParams.productModel"
|
placeholder="产品规格"
|
clearable
|
@change="handleSearch" />
|
<view class="filter-status"
|
@click="showStatusSheet = true">
|
<text :class="{ placeholder: statusText === '' }">{{ statusText || "全部状态" }}</text>
|
<up-icon name="arrow-down"
|
size="14"
|
color="#999"></up-icon>
|
</view>
|
</view>
|
<view class="filter-actions-top">
|
<up-button type="primary"
|
size="small"
|
text="搜索"
|
@click="handleSearch"></up-button>
|
</view>
|
</view>
|
|
<!-- 列表区域 -->
|
<view v-if="list.length > 0"
|
class="ledger-list">
|
<view v-for="item in list"
|
:key="item.id"
|
class="ledger-item">
|
<view class="item-header">
|
<view class="item-left">
|
<view class="document-icon">
|
<up-icon name="setting"
|
size="16"
|
color="#ffffff"></up-icon>
|
</view>
|
<text class="item-id">{{ item.productModel || "-" }}</text>
|
</view>
|
<up-tag :text="item.status == 1 ? '启用' : '停用'"
|
:type="item.status == 1 ? 'success' : 'info'"
|
size="mini" />
|
</view>
|
<up-divider></up-divider>
|
<view class="item-details">
|
<view class="detail-row">
|
<text class="detail-label">计件单价</text>
|
<text class="detail-value highlight">¥{{ Number(item.unitPrice) || 0 }} / 件</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">工序名称</text>
|
<text class="detail-value">{{ item.operationName || "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">备注</text>
|
<text class="detail-value">{{ item.remark || "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">创建时间</text>
|
<text class="detail-value">{{ item.createTime || "-" }}</text>
|
</view>
|
</view>
|
<view class="action-buttons">
|
<up-button class="action-btn"
|
size="small"
|
:type="item.status == 1 ? 'warning' : 'success'"
|
@click="handleChangeStatus(item)">{{ item.status == 1 ? "停用" : "启用" }}</up-button>
|
<up-button class="action-btn"
|
size="small"
|
type="primary"
|
@click="handleEdit(item)">编辑</up-button>
|
<up-button class="action-btn"
|
size="small"
|
type="error"
|
@click="handleDelete(item)">删除</up-button>
|
</view>
|
</view>
|
<up-loadmore :status="pageStatus" />
|
</view>
|
<view v-else
|
class="no-data">
|
<up-empty mode="data"
|
text="暂无数据"></up-empty>
|
</view>
|
|
<!-- 新增按钮 -->
|
<view class="fab-button"
|
@click="handleAdd">
|
<up-icon name="plus"
|
size="28"
|
color="#ffffff"></up-icon>
|
</view>
|
|
<!-- 新增/编辑弹窗 -->
|
<up-popup v-model:show="showForm"
|
mode="bottom"
|
:round="20"
|
:safeAreaInsetBottom="true"
|
@close="showForm = false">
|
<view class="form-popup">
|
<view class="popup-header">
|
<text class="popup-title">{{ form.id ? "编辑计件单价" : "新增计件单价" }}</text>
|
</view>
|
<view class="popup-body">
|
<up-form labelWidth="120">
|
<up-form-item label="产品"
|
required>
|
<view class="pick-field"
|
@click="showProductTree = true">
|
<up-input class="pick-input"
|
v-model="form.productName"
|
placeholder="请选择产品"
|
readonly />
|
<up-icon name="arrow-right"
|
color="#999999"></up-icon>
|
</view>
|
</up-form-item>
|
<up-form-item label="规格型号"
|
required>
|
<view class="pick-field"
|
@click="openModelSheet">
|
<up-input class="pick-input"
|
v-model="form.model"
|
placeholder="请选择规格型号"
|
readonly />
|
<up-icon name="arrow-right"
|
color="#999999"></up-icon>
|
</view>
|
</up-form-item>
|
<up-form-item label="工序名称"
|
required>
|
<up-input v-model="form.operationName"
|
placeholder="请输入工序名称"
|
clearable />
|
</up-form-item>
|
<up-form-item label="计件单价"
|
required>
|
<up-input v-model="form.unitPrice"
|
type="number"
|
placeholder="请输入单价(元/件)" />
|
<text class="unit-text">元/件</text>
|
</up-form-item>
|
<up-form-item label="备注">
|
<up-input v-model="form.remark"
|
placeholder="请输入备注"
|
clearable />
|
</up-form-item>
|
</up-form>
|
</view>
|
<view class="popup-footer">
|
<up-button text="取消"
|
type="info"
|
plain
|
:customStyle="{ flex: 1, marginRight: '12px' }"
|
@click="showForm = false" />
|
<up-button text="保存"
|
type="primary"
|
:customStyle="{ flex: 1 }"
|
:loading="submitting"
|
@click="handleSubmit" />
|
</view>
|
</view>
|
</up-popup>
|
|
<!-- 产品树选择器 -->
|
<up-popup v-model:show="showProductTree"
|
mode="bottom"
|
:round="true"
|
:closeable="true"
|
@close="showProductTree = false">
|
<view class="tree-selector">
|
<view class="tree-header">
|
<text class="tree-title">选择产品</text>
|
</view>
|
<scroll-view scroll-y
|
class="tree-content">
|
<view v-for="(node, index) in productOptions"
|
:key="index"
|
class="tree-node">
|
<view v-if="node.children && node.children.length > 0"
|
class="tree-node-header"
|
@click="toggleNode(node)">
|
<up-icon :name="node.expanded ? 'arrow-down' : 'arrow-right'"
|
class="tree-node-icon" />
|
<text class="tree-node-label">{{ node.label }}</text>
|
</view>
|
<view v-else
|
class="tree-node-header"
|
@click="selectTreeNode(node)">
|
<text class="tree-node-icon-placeholder"></text>
|
<text class="tree-node-label">{{ node.label }}</text>
|
<up-icon name="checkmark"
|
v-if="form.productId == node.value"
|
class="tree-node-check" />
|
</view>
|
<view v-if="node.children && node.children.length > 0 && node.expanded"
|
class="tree-node-children">
|
<view v-for="(child, childIndex) in node.children"
|
:key="childIndex"
|
class="tree-node">
|
<view v-if="child.children && child.children.length > 0"
|
class="tree-node-header"
|
@click="toggleNode(child)">
|
<up-icon :name="child.expanded ? 'arrow-down' : 'arrow-right'"
|
class="tree-node-icon" />
|
<text class="tree-node-label">{{ child.label }}</text>
|
</view>
|
<view v-else
|
class="tree-node-header"
|
@click="selectTreeNode(child)">
|
<text class="tree-node-icon-placeholder"></text>
|
<text class="tree-node-label">{{ child.label }}</text>
|
<up-icon name="checkmark"
|
v-if="form.productId == child.value"
|
class="tree-node-check" />
|
</view>
|
<view v-if="child.children && child.children.length > 0 && child.expanded"
|
class="tree-node-children">
|
<view v-for="(leaf, leafIndex) in child.children"
|
:key="leafIndex"
|
class="tree-node-header"
|
@click="selectTreeNode(leaf)">
|
<text class="tree-node-icon-placeholder"></text>
|
<text class="tree-node-label">{{ leaf.label }}</text>
|
<up-icon name="checkmark"
|
v-if="form.productId == leaf.value"
|
class="tree-node-check" />
|
</view>
|
</view>
|
</view>
|
</view>
|
</view>
|
</scroll-view>
|
</view>
|
</up-popup>
|
|
<!-- 规格型号选择器 -->
|
<up-action-sheet :show="showModelSheet"
|
:actions="modelOptions"
|
title="选择规格型号"
|
@select="selectModel"
|
@close="showModelSheet = false" />
|
|
<!-- 状态筛选 -->
|
<up-action-sheet :show="showStatusSheet"
|
:actions="statusOptions"
|
title="选择状态"
|
@select="selectStatus"
|
@close="showStatusSheet = false" />
|
</view>
|
</template>
|
|
<script setup>
|
import { ref, reactive } from "vue";
|
import { onReachBottom, onShow } from "@dcloudio/uni-app";
|
import PageHeader from "@/components/PageHeader.vue";
|
import { productTreeList, modelList } from "@/api/basicData/product.js";
|
import {
|
listPage,
|
add,
|
update,
|
changeStatus,
|
batchDelete,
|
} from "@/api/productionManagement/pieceRateConfig.js";
|
|
const goBack = () => {
|
uni.navigateBack();
|
};
|
|
// 列表与分页
|
const list = ref([]);
|
const pageStatus = ref("loadmore");
|
const loading = ref(false);
|
const page = reactive({ current: 1, size: 10, total: 0 });
|
|
const queryParams = reactive({
|
operationName: "",
|
productModel: "",
|
status: "",
|
});
|
const statusText = ref("");
|
|
// 状态筛选选项
|
const statusOptions = [
|
{ name: "全部状态", value: "" },
|
{ name: "启用", value: 1 },
|
{ name: "停用", value: 0 },
|
];
|
|
const showStatusSheet = ref(false);
|
|
const selectStatus = e => {
|
queryParams.status = e.value;
|
statusText.value = e.name === "全部状态" ? "" : e.name;
|
showStatusSheet.value = false;
|
handleSearch();
|
};
|
|
// 兼容不同层级的列表返回结构
|
const pickList = res => {
|
const rows = res?.data?.records || res?.data || res?.records || res;
|
return Array.isArray(rows) ? rows : [];
|
};
|
|
const getList = () => {
|
if (loading.value || pageStatus.value === "loading") return;
|
loading.value = true;
|
pageStatus.value = "loading";
|
const params = {
|
current: page.current,
|
size: page.size,
|
operationName: queryParams.operationName || undefined,
|
productModel: queryParams.productModel || undefined,
|
status: queryParams.status === "" ? undefined : queryParams.status,
|
};
|
listPage(params)
|
.then(res => {
|
const records = res?.data?.records || res?.records || [];
|
const total = res?.data?.total || res?.total || 0;
|
list.value =
|
page.current === 1 ? records : [...list.value, ...records];
|
page.total = total;
|
if (list.value.length >= total || records.length < page.size) {
|
pageStatus.value = "nomore";
|
} else {
|
pageStatus.value = "loadmore";
|
page.current++;
|
}
|
})
|
.catch(() => {
|
pageStatus.value = "loadmore";
|
})
|
.finally(() => {
|
loading.value = false;
|
});
|
};
|
|
const handleSearch = () => {
|
page.current = 1;
|
list.value = [];
|
pageStatus.value = "loadmore";
|
getList();
|
};
|
|
// ============ 表单 ============
|
const showForm = ref(false);
|
const submitting = ref(false);
|
const form = ref({
|
id: null,
|
productId: "",
|
productName: "",
|
productModelId: "",
|
model: "",
|
operationName: "",
|
unitPrice: "",
|
remark: "",
|
});
|
|
// 产品树
|
const showProductTree = ref(false);
|
const productOptions = ref([]);
|
const showModelSheet = ref(false);
|
const modelOptions = ref([]);
|
|
const convertIdToValue = data => {
|
return (data || []).map(item => {
|
const { id, children, ...rest } = item;
|
const newItem = { ...rest, value: id };
|
if (children && children.length > 0) {
|
newItem.children = convertIdToValue(children);
|
}
|
return newItem;
|
});
|
};
|
|
const findNodeById = (nodes, id) => {
|
for (let i = 0; i < nodes.length; i++) {
|
if (nodes[i].value === id) return nodes[i].label;
|
if (nodes[i].children && nodes[i].children.length > 0) {
|
const found = findNodeById(nodes[i].children, id);
|
if (found) return found;
|
}
|
}
|
return null;
|
};
|
|
const getProductOptions = () => {
|
if (productOptions.value.length > 0) return;
|
productTreeList()
|
.then(res => {
|
productOptions.value = convertIdToValue(pickList(res));
|
})
|
.catch(() => {});
|
};
|
|
const toggleNode = node => {
|
node.expanded = !node.expanded;
|
};
|
|
const selectTreeNode = node => {
|
if (node.children && node.children.length > 0) return;
|
form.value.productId = node.value;
|
form.value.productName = node.label;
|
showProductTree.value = false;
|
getModels(node.value);
|
};
|
|
const getModels = productId => {
|
form.value.productModelId = "";
|
form.value.model = "";
|
modelOptions.value = [];
|
modelList({ id: productId })
|
.then(res => {
|
modelOptions.value = pickList(res).map(item => ({
|
name: item.model,
|
value: item.id,
|
}));
|
})
|
.catch(() => {
|
modelOptions.value = [];
|
});
|
};
|
|
const openModelSheet = () => {
|
if (!form.value.productId) {
|
uni.showToast({ title: "请先选择产品", icon: "none" });
|
return;
|
}
|
showModelSheet.value = true;
|
};
|
|
const selectModel = e => {
|
form.value.productModelId = e.value;
|
form.value.model = e.name;
|
showModelSheet.value = false;
|
};
|
|
const handleAdd = () => {
|
form.value = {
|
id: null,
|
productId: "",
|
productName: "",
|
productModelId: "",
|
model: "",
|
operationName: "",
|
unitPrice: "",
|
remark: "",
|
};
|
getProductOptions();
|
showForm.value = true;
|
};
|
|
const handleEdit = item => {
|
form.value = {
|
id: item.id,
|
productId: item.productId || "",
|
productName: item.productName || "",
|
productModelId: item.productModelId || "",
|
model: item.productModel || "",
|
operationName: item.operationName || "",
|
unitPrice: item.unitPrice ?? "",
|
remark: item.remark || "",
|
};
|
getProductOptions();
|
if (item.productModelId) {
|
modelList({ id: item.productId })
|
.then(res => {
|
modelOptions.value = pickList(res).map(m => ({
|
name: m.model,
|
value: m.id,
|
}));
|
})
|
.catch(() => {});
|
}
|
showForm.value = true;
|
};
|
|
const handleSubmit = () => {
|
if (!form.value.productModelId) {
|
uni.showToast({ title: "请选择规格型号", icon: "none" });
|
return;
|
}
|
if (!form.value.operationName) {
|
uni.showToast({ title: "请输入工序名称", icon: "none" });
|
return;
|
}
|
const unitPrice = Number(form.value.unitPrice);
|
if (!(unitPrice > 0)) {
|
uni.showToast({ title: "计件单价必须大于0", icon: "none" });
|
return;
|
}
|
submitting.value = true;
|
const payload = {
|
productModelId: form.value.productModelId,
|
productModel: form.value.model,
|
operationName: form.value.operationName,
|
unitPrice: unitPrice,
|
remark: form.value.remark,
|
};
|
const req = form.value.id
|
? update({ ...payload, id: form.value.id })
|
: add(payload);
|
req
|
.then(() => {
|
uni.showToast({ title: form.value.id ? "修改成功" : "新增成功" });
|
showForm.value = false;
|
handleSearch();
|
})
|
.catch(() => {})
|
.finally(() => {
|
submitting.value = false;
|
});
|
};
|
|
const handleChangeStatus = item => {
|
const nextStatus = item.status == 1 ? 0 : 1;
|
changeStatus({ id: item.id, status: nextStatus })
|
.then(() => {
|
uni.showToast({ title: nextStatus == 1 ? "已启用" : "已停用" });
|
handleSearch();
|
})
|
.catch(() => {});
|
};
|
|
const handleDelete = item => {
|
uni.showModal({
|
title: "提示",
|
content: "确定要删除该计件单价配置吗?",
|
success: res => {
|
if (res.confirm) {
|
batchDelete([item.id])
|
.then(() => {
|
uni.showToast({ title: "删除成功" });
|
handleSearch();
|
})
|
.catch(() => {});
|
}
|
},
|
});
|
};
|
|
onReachBottom(() => {
|
if (pageStatus.value === "nomore") return;
|
if (pageStatus.value === "loading") return;
|
getList();
|
});
|
|
onShow(() => {
|
handleSearch();
|
});
|
</script>
|
|
<style scoped lang="scss">
|
@import "@/styles/procurement-common.scss";
|
|
.filter-row {
|
display: flex;
|
align-items: center;
|
gap: 12px;
|
|
.filter-input {
|
flex: 1;
|
background: #f5f5f5;
|
border-radius: 8px;
|
padding: 0 12px;
|
height: 40px;
|
}
|
|
.filter-status {
|
flex: 1;
|
height: 40px;
|
background: #f5f5f5;
|
border-radius: 8px;
|
padding: 0 12px;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
font-size: 14px;
|
color: #333;
|
|
.placeholder {
|
color: #999;
|
}
|
}
|
}
|
|
.filter-actions-top {
|
display: flex;
|
justify-content: flex-end;
|
gap: 12px;
|
margin-top: 12px;
|
}
|
|
.form-popup {
|
background: #fff;
|
|
.popup-header {
|
padding: 16px;
|
border-bottom: 1px solid #f5f5f5;
|
text-align: center;
|
}
|
|
.popup-title {
|
font-size: 16px;
|
font-weight: 500;
|
color: #333;
|
}
|
|
.popup-body {
|
padding: 8px 16px;
|
max-height: 60vh;
|
overflow-y: auto;
|
}
|
|
.unit-text {
|
margin-left: 8px;
|
color: #909399;
|
font-size: 12px;
|
}
|
|
// 选择类字段:readonly 的 up-input 会把原生 input 置为 disabled,
|
// disabled 元素会吞掉点击事件导致外层收不到,故让 input 不参与指针事件,由 .pick-field 接管
|
.pick-field {
|
flex: 1;
|
display: flex;
|
align-items: center;
|
|
.pick-input {
|
flex: 1;
|
}
|
|
:deep(input) {
|
pointer-events: none;
|
}
|
}
|
|
.popup-footer {
|
display: flex;
|
padding: 12px 16px 20px;
|
}
|
}
|
|
.tree-selector {
|
padding: 16px;
|
|
.tree-header {
|
text-align: center;
|
padding-bottom: 12px;
|
border-bottom: 1px solid #f5f5f5;
|
}
|
|
.tree-title {
|
font-size: 16px;
|
font-weight: 500;
|
color: #333;
|
}
|
|
.tree-content {
|
max-height: 60vh;
|
}
|
|
.tree-node-header {
|
display: flex;
|
align-items: center;
|
padding: 12px 4px;
|
border-bottom: 1px solid #f7f7f7;
|
|
.tree-node-icon {
|
margin-right: 8px;
|
}
|
|
.tree-node-icon-placeholder {
|
width: 16px;
|
margin-right: 8px;
|
}
|
|
.tree-node-label {
|
flex: 1;
|
font-size: 14px;
|
color: #333;
|
}
|
}
|
|
.tree-node-children {
|
padding-left: 24px;
|
}
|
}
|
</style>
|