完善基础数据

This commit is contained in:
yanpeng200212
2026-10-08 15:44:07 +08:00
parent 8430ba0452
commit 2ffb83830c
95 changed files with 37759 additions and 5 deletions
+6
View File
@@ -52,6 +52,12 @@ export const componentMap = {
'base/guige/index': () => import('../views/base/guige/index.vue'),
'base/dingjizhibiao/index': () => import('../views/base/dingjizhibiao/index.vue'),
'base/jizuchetai/index': () => import('../views/base/jizuchetai/index.vue'),
'base/worktype/index': () => import('../views/base/worktype/index.vue'),
'base/zhangqi/index': () => import('../views/base/zhangqi/index.vue'),
'base/registry/index': () => import('../views/base/registry/index.vue'),
'base/yuanliaozhuangkou/index': () => import('../views/base/yuanliaozhuangkou/index.vue'),
'base/wuzimulu/index': () => import('../views/base/wuzimulu/index.vue'),
'base/gongyingshang/index': () => import('../views/base/gongyingshang/index.vue'),
'prod/lsjl/index': () => import('../views/prod/lsjl/index.vue'),
'prod/xdjy/index': () => import('../views/prod/xdjy/index.vue'),
'prod/hbjy/index': () => import('../views/prod/hbjy/index.vue'),
+257
View File
@@ -0,0 +1,257 @@
<script setup>
import { ref, reactive, onMounted } from 'vue';
import request from '../../../utils/request';
import { ElMessage, ElMessageBox } from 'element-plus';
// 业务类型 / 分组(与后端 Gys_Type_Enum / Gys_Group_Enum 一致)
const typeOptions = [
{ label: '干茧', value: 1 },
{ label: '配件', value: 2 },
{ label: '副产品', value: 3 },
{ label: '生丝', value: 4 }
];
const groupOptions = [
{ label: '供应商', value: 1 },
{ label: '客户', value: 2 }
];
// 销售类型(Gys_SaleType_Enum:内销=1、外销=2;历史数据中的 0 表示未设置)
const saleTypeOptions = [
{ label: '未设置', value: 0 },
{ label: '内销', value: 1 },
{ label: '外销', value: 2 }
];
const list = ref([]);
const total = ref(0);
const loading = ref(false);
const query = reactive({ type: 1, saleType: null, like: '', pageIndex: 1, pageSize: 20 });
const dialogVisible = ref(false);
const dialogTitle = ref('新增往来单位');
const isEdit = ref(false);
const editId = ref(null);
const form = reactive({
name: '',
type: 1,
group: 1,
saleType: 0,
lianxiren: '',
phone: '',
address: '',
pinyin: ''
});
const vendorGroupTypes = [3, 4]; // 副产品 / 生丝 → 默认分组为"客户"
async function loadList() {
loading.value = true;
try {
const res = await request.get('/sys/gongyingshang', {
params: {
type: query.type,
saleType: query.saleType === null ? undefined : query.saleType,
like: query.like || undefined,
pageIndex: query.pageIndex,
pageSize: query.pageSize
}
});
list.value = res.data.items || [];
total.value = res.data.total || 0;
} finally {
loading.value = false;
}
}
function handleSearch() {
query.pageIndex = 1;
loadList();
}
function handleTypeChange() {
if (vendorGroupTypes.includes(form.type)) form.group = 2;
else form.group = 1;
}
function openCreate() {
isEdit.value = false;
dialogTitle.value = '新增往来单位';
Object.assign(form, {
name: '',
type: 1,
group: 1,
saleType: 0,
lianxiren: '',
phone: '',
address: '',
pinyin: ''
});
dialogVisible.value = true;
}
function openEdit(row) {
isEdit.value = true;
editId.value = row.id;
dialogTitle.value = `修改往来单位「${row.name}」`;
Object.assign(form, {
name: row.name,
type: Number(row.type),
group: Number(row.group),
saleType: row.saleType,
lianxiren: row.lianxiren || '',
phone: row.phone || '',
address: row.address || '',
pinyin: row.pinyin || ''
});
dialogVisible.value = true;
}
async function handleSave() {
if (!form.name) {
ElMessage.warning('请填写往来单位名称');
return;
}
const payload = {
name: form.name,
type: form.type,
group: form.group,
saleType: form.saleType,
lianxiren: form.lianxiren,
phone: form.phone,
address: form.address,
pinyin: form.pinyin
};
try {
if (isEdit.value) {
await request.put(`/sys/gongyingshang/${editId.value}`, payload);
ElMessage.success('修改成功');
} else {
await request.post('/sys/gongyingshang', payload);
ElMessage.success('新增成功');
}
dialogVisible.value = false;
loadList();
} catch (e) {}
}
async function handleDelete(row) {
await ElMessageBox.confirm(`确定删除「${row.name}」吗?`, '提示', { type: 'warning' });
try {
await request.delete(`/sys/gongyingshang/${row.id}`);
ElMessage.success('删除成功');
loadList();
} catch (e) {}
}
onMounted(loadList);
</script>
<template>
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>往来单位</span>
<el-button type="primary" @click="openCreate">新增往来单位</el-button>
</div>
</template>
<div class="search-bar">
<el-select v-model="query.type" style="width: 120px" @change="handleSearch">
<el-option v-for="t in typeOptions" :key="t.value" :label="t.label" :value="t.value" />
</el-select>
<el-select v-model="query.saleType" placeholder="销售类型(全部)" clearable style="width: 150px" @change="handleSearch">
<el-option v-for="s in saleTypeOptions" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
<el-input
v-model="query.like"
placeholder="名称 / 简拼 / 联系人 / 电话"
clearable
style="width: 240px"
@keyup.enter="handleSearch"
@clear="handleSearch"
/>
<el-button type="primary" @click="handleSearch">查询</el-button>
</div>
<el-table v-loading="loading" :data="list" border stripe>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="name" label="往来单位" min-width="220" />
<el-table-column prop="grouname" label="分组" width="90" />
<el-table-column prop="saleTypeName" label="销售类型" width="100" />
<el-table-column prop="lianxiren" label="联系人" width="100" />
<el-table-column prop="phone" label="电话" width="130" />
<el-table-column prop="address" label="地址" min-width="180" show-overflow-tooltip />
<el-table-column prop="pinyin" label="简拼" width="90" />
<el-table-column label="操作" width="140" fixed="right">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="openEdit(row)">编辑</el-button>
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="query.pageIndex"
v-model:page-size="query.pageSize"
:total="total"
layout="total, prev, pager, next"
class="pager"
@current-change="loadList"
/>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="520px">
<el-form :model="form" label-width="100px">
<el-form-item label="名称" required>
<el-input v-model="form.name" placeholder="往来单位名称" />
</el-form-item>
<el-form-item label="业务类型" required>
<el-select v-model="form.type" style="width: 100%" @change="handleTypeChange">
<el-option v-for="t in typeOptions" :key="t.value" :label="t.label" :value="t.value" />
</el-select>
</el-form-item>
<el-form-item label="分组">
<el-select v-model="form.group" style="width: 100%">
<el-option v-for="g in groupOptions" :key="g.value" :label="g.label" :value="g.value" />
</el-select>
</el-form-item>
<el-form-item label="销售类型">
<el-select v-model="form.saleType" style="width: 100%">
<el-option v-for="s in saleTypeOptions" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
<el-form-item label="联系人">
<el-input v-model="form.lianxiren" />
</el-form-item>
<el-form-item label="电话">
<el-input v-model="form.phone" />
</el-form-item>
<el-form-item label="地址">
<el-input v-model="form.address" />
</el-form-item>
<el-form-item label="简拼">
<el-input v-model="form.pinyin" placeholder="如 HD" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave">确定</el-button>
</template>
</el-dialog>
</el-card>
</template>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.search-bar {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.pager {
margin-top: 12px;
justify-content: flex-end;
}
</style>
+335
View File
@@ -0,0 +1,335 @@
<script setup>
import { ref, reactive, computed, onMounted } from 'vue';
import request from '../../../utils/request';
import { ElMessage, ElMessageBox } from 'element-plus';
const allData = ref([]);
const treeData = ref([]);
const loading = ref(false);
const selectedPath = ref('');
const treeRef = ref(null);
const query = reactive({ keyword: '', pageIndex: 1, pageSize: 20 });
const pagedList = ref([]);
const filteredTotal = ref(0);
const dataTypeOptions = ['string', 'int', 'decimal', 'bool', 'json', 'array'];
const dialogVisible = ref(false);
const dialogTitle = ref('新增配置项');
const isEdit = ref(false);
const editId = ref(null);
const form = reactive({
category: '',
keyName: '',
value: '',
dataType: 'string',
description: '',
isActive: true
});
// 按命名空间(反斜杠分层)构建树,并统计各节点(含子级)配置项数量
function buildTree(list) {
const roots = [];
const map = {};
const paths = [...new Set(list.map((x) => (x.category || '').trim()).filter(Boolean))].sort();
paths.forEach((path) => {
const parts = path.split('\\');
let cur = '';
parts.forEach((part, i) => {
cur = i === 0 ? part : `${cur}\\${part}`;
if (!map[cur]) {
const node = { id: cur, label: part, children: [] };
map[cur] = node;
if (i === 0) roots.push(node);
else map[parts.slice(0, i).join('\\')].children.push(node);
}
});
});
const countOf = (path) =>
list.filter((x) => x.category === path || (x.category || '').startsWith(`${path}\\`)).length;
Object.values(map).forEach((node) => {
node.label = `${node.label} (${countOf(node.id)})`;
});
return roots;
}
function applyFilter() {
let data = allData.value;
if (selectedPath.value) {
data = data.filter(
(x) => x.category === selectedPath.value || (x.category || '').startsWith(`${selectedPath.value}\\`)
);
}
const kw = (query.keyword || '').trim();
if (kw) {
data = data.filter(
(x) =>
(x.keyName || '').includes(kw) ||
(x.value || '').includes(kw) ||
(x.description || '').includes(kw) ||
(x.category || '').includes(kw)
);
}
filteredTotal.value = data.length;
const start = (query.pageIndex - 1) * query.pageSize;
pagedList.value = data.slice(start, start + query.pageSize);
}
async function loadAll() {
loading.value = true;
try {
const res = await request.get('/sys/registry');
allData.value = res.data || [];
treeData.value = buildTree(allData.value);
applyFilter();
} finally {
loading.value = false;
}
}
function handleNodeClick(node) {
selectedPath.value = node.id;
query.pageIndex = 1;
applyFilter();
}
function handleClearNode() {
selectedPath.value = '';
treeRef.value?.setCurrentKey(null);
query.pageIndex = 1;
applyFilter();
}
function handleSearch() {
query.pageIndex = 1;
applyFilter();
}
function handlePageChange(page) {
query.pageIndex = page;
applyFilter();
}
function openCreate() {
isEdit.value = false;
dialogTitle.value = '新增配置项';
Object.assign(form, {
category: selectedPath.value || '',
keyName: '',
value: '',
dataType: 'string',
description: '',
isActive: true
});
dialogVisible.value = true;
}
function openEdit(row) {
isEdit.value = true;
editId.value = row.id;
dialogTitle.value = '修改配置项';
Object.assign(form, {
category: row.category,
keyName: row.keyName,
value: row.value,
dataType: row.dataType || 'string',
description: row.description || '',
isActive: row.isActive
});
dialogVisible.value = true;
}
async function handleSave() {
if (!form.category) {
ElMessage.warning('请填写命名空间');
return;
}
if (!form.keyName) {
ElMessage.warning('请填写键名');
return;
}
const payload = {
category: form.category.trim(),
keyName: form.keyName.trim(),
value: form.value ?? '',
dataType: form.dataType,
description: form.description || '',
isActive: form.isActive
};
try {
if (isEdit.value) {
await request.put(`/sys/registry/${editId.value}`, payload);
ElMessage.success('修改成功');
} else {
await request.post('/sys/registry', payload);
ElMessage.success('保存成功');
}
dialogVisible.value = false;
loadAll();
} catch (e) {}
}
async function handleDelete(row) {
await ElMessageBox.confirm(`确定删除配置项「${row.keyName}」吗?`, '提示', { type: 'warning' });
try {
await request.delete(`/sys/registry/${row.id}`);
ElMessage.success('删除成功');
loadAll();
} catch (e) {}
}
const currentPathLabel = computed(() => selectedPath.value || '全部配置项');
onMounted(loadAll);
</script>
<template>
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>注册表</span>
<el-button type="primary" @click="openCreate">新增配置项</el-button>
</div>
</template>
<div class="registry-body">
<div class="tree-panel">
<div class="tree-title">
<span>命名空间</span>
<el-button size="small" link type="primary" @click="handleClearNode">全部</el-button>
</div>
<el-tree
ref="treeRef"
:data="treeData"
node-key="id"
highlight-current
default-expand-all
:props="{ label: 'label', children: 'children' }"
@node-click="handleNodeClick"
/>
</div>
<div class="table-panel">
<div class="search-bar">
<el-input
v-model="query.keyword"
placeholder="键名 / 值 / 描述 / 命名空间"
clearable
style="width: 260px"
@keyup.enter="handleSearch"
@clear="handleSearch"
/>
<el-button type="primary" @click="handleSearch">查询</el-button>
<span class="path-tip">当前:{{ currentPathLabel }}</span>
</div>
<el-table v-loading="loading" :data="pagedList" border stripe>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="keyName" label="键名" min-width="150" />
<el-table-column prop="value" label="值" min-width="220" show-overflow-tooltip />
<el-table-column prop="dataType" label="数据类型" width="100" />
<el-table-column prop="description" label="描述" min-width="140" show-overflow-tooltip />
<el-table-column label="是否启用" width="90">
<template #default="{ row }">
<el-tag :type="row.isActive ? 'success' : 'info'">{{ row.isActive ? '启用' : '禁用' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="category" label="命名空间" min-width="160" show-overflow-tooltip />
<el-table-column label="操作" width="140" fixed="right">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="openEdit(row)">编辑</el-button>
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
:current-page="query.pageIndex"
:page-size="query.pageSize"
:total="filteredTotal"
layout="total, prev, pager, next"
class="pager"
@current-change="handlePageChange"
/>
</div>
</div>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="520px">
<el-form :model="form" label-width="100px">
<el-form-item label="命名空间" required>
<el-input v-model="form.category" placeholder="如:基础数据\工种" />
</el-form-item>
<el-form-item label="键名" required>
<el-input v-model="form.keyName" placeholder="如:业务类型" />
</el-form-item>
<el-form-item label="值">
<el-input v-model="form.value" type="textarea" :rows="3" placeholder="数组类型存逗号分隔字符串;json 类型存 JSON 文本" />
</el-form-item>
<el-form-item label="数据类型">
<el-select v-model="form.dataType" style="width: 100%">
<el-option v-for="t in dataTypeOptions" :key="t" :label="t" :value="t" />
</el-select>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="form.description" placeholder="配置说明" />
</el-form-item>
<el-form-item label="是否启用">
<el-switch v-model="form.isActive" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave">确定</el-button>
</template>
</el-dialog>
</el-card>
</template>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.registry-body {
display: flex;
gap: 12px;
}
.tree-panel {
width: 240px;
flex-shrink: 0;
border-right: 1px solid var(--el-border-color-lighter);
padding-right: 8px;
max-height: 620px;
overflow: auto;
}
.tree-title {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
margin-bottom: 8px;
}
.table-panel {
flex: 1;
min-width: 0;
}
.search-bar {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 12px;
}
.path-tip {
color: var(--el-text-color-secondary);
font-size: 12px;
}
.pager {
margin-top: 12px;
justify-content: flex-end;
}
</style>
+335
View File
@@ -0,0 +1,335 @@
<script setup>
import { ref, reactive, nextTick, onMounted } from 'vue';
import request from '../../../utils/request';
import { buildDeptTree } from '../../../utils/dept';
import { ElMessage, ElMessageBox } from 'element-plus';
const list = ref([]);
const total = ref(0);
const loading = ref(false);
const query = reactive({ pageIndex: 1, pageSize: 20, keyword: '' });
// 下拉选项来自注册表「基础数据\工种」(业务类型 / 生产成本分类),未配置时用默认值兜底
const bizTypeOptions = ref(['生产', '辅助']);
const sccbFenleiOptions = ref(['无', '管理']);
async function loadRegistryOptions() {
try {
const load = async (keyName) => {
const res = await request.get('/sys/registry/by-key', {
params: { category: '基础数据\\工种', keyName }
});
return (res.data?.value || '')
.split(',')
.map((s) => s.trim())
.filter(Boolean);
};
const [biz, fenlei] = await Promise.all([load('业务类型'), load('生产成本分类')]);
if (biz.length) bizTypeOptions.value = biz;
if (fenlei.length) sccbFenleiOptions.value = fenlei;
} catch (e) {
// 注册表未配置时保持默认选项
}
}
// 新增/编辑
const dialogVisible = ref(false);
const dialogTitle = ref('新增工种');
const isEdit = ref(false);
const editId = ref(null);
const form = reactive({ workName: '', sortOrder: 100, bizType: '生产', sccbFenlei: '无' });
// 绑定部门抽屉
const deptDrawerVisible = ref(false);
const deptTree = ref([]);
const deptTreeRef = ref(null);
const boundDeptIds = ref([]);
// 关联角色抽屉
const roleDrawerVisible = ref(false);
const roleList = ref([]);
const boundRoleIds = ref([]);
const currentRow = ref(null);
async function loadList() {
loading.value = true;
try {
const res = await request.get('/sys/worktype', { params: query });
list.value = res.data.items || [];
total.value = res.data.total || 0;
} finally {
loading.value = false;
}
}
function handleSearch() {
query.pageIndex = 1;
loadList();
}
function openCreate() {
isEdit.value = false;
dialogTitle.value = '新增工种';
Object.assign(form, { workName: '', sortOrder: 100, bizType: '生产', sccbFenlei: '无' });
dialogVisible.value = true;
}
function openEdit(row) {
isEdit.value = true;
editId.value = row.id;
dialogTitle.value = '修改工种';
Object.assign(form, {
workName: row.workName,
sortOrder: row.sortOrder,
bizType: row.bizType,
sccbFenlei: row.sccbFenlei
});
dialogVisible.value = true;
}
async function handleSave() {
if (!form.workName) {
ElMessage.warning('请填写工种名称');
return;
}
try {
if (isEdit.value) {
await request.put(`/sys/worktype/${editId.value}`, form);
ElMessage.success('修改成功');
} else {
await request.post('/sys/worktype', form);
ElMessage.success('新增成功');
}
dialogVisible.value = false;
loadList();
} catch (e) {}
}
async function handleDelete(row) {
await ElMessageBox.confirm(`确定删除工种「${row.workName}」吗?`, '提示', { type: 'warning' });
try {
await request.delete(`/sys/worktype/${row.id}`);
ElMessage.success('删除成功');
loadList();
} catch (e) {}
}
async function handleActive(row) {
try {
await request.put(`/sys/worktype/${row.id}/active`, { active: !row.isActive });
ElMessage.success('操作成功');
loadList();
} catch (e) {}
}
// ==================== 绑定部门 ====================
async function openDeptDrawer(row) {
currentRow.value = row;
deptDrawerVisible.value = true;
try {
if (!deptTree.value.length) {
const deps = await request.get('/sys/department');
deptTree.value = buildDeptTree(deps.data);
}
const res = await request.get(`/sys/worktype/${row.id}/departments`);
boundDeptIds.value = (res.data || []).map((x) => String(x.departmentId));
await nextTick();
deptTreeRef.value?.setCheckedKeys(boundDeptIds.value);
} catch (e) {}
}
async function handleDeptCheck(node, { checkedKeys }) {
if (!currentRow.value) return;
const workTypeId = currentRow.value.id;
const current = (checkedKeys || []).map(String);
const existing = boundDeptIds.value.map(String);
const added = current.filter((k) => !existing.includes(k));
const removed = existing.filter((k) => !current.includes(k));
if (!added.length && !removed.length) return;
try {
for (const id of added) {
await request.post(`/sys/worktype/${workTypeId}/departments`, { departmentId: id });
}
for (const id of removed) {
await request.delete(`/sys/worktype/${workTypeId}/departments/${id}`);
}
boundDeptIds.value = current;
ElMessage.success('绑定成功');
loadList();
} catch (e) {
await nextTick();
deptTreeRef.value?.setCheckedKeys(boundDeptIds.value);
}
}
// ==================== 关联角色 ====================
async function openRoleDrawer(row) {
currentRow.value = row;
roleDrawerVisible.value = true;
try {
if (!roleList.value.length) {
const roles = await request.get('/sys/role');
roleList.value = roles.data || [];
}
const res = await request.get(`/sys/worktype/${row.id}/roles`);
boundRoleIds.value = (res.data || []).map((x) => String(x.roleId));
} catch (e) {}
}
async function handleRoleChange(keys) {
if (!currentRow.value) return;
const workTypeId = currentRow.value.id;
const current = (keys || []).map(String);
const added = current.filter((k) => !boundRoleIds.value.includes(k));
const removed = boundRoleIds.value.filter((k) => !current.includes(k));
try {
for (const id of added) {
await request.post(`/sys/worktype/${workTypeId}/roles`, { roleId: id });
}
for (const id of removed) {
await request.delete(`/sys/worktype/${workTypeId}/roles/${id}`);
}
boundRoleIds.value = current;
ElMessage.success('关联成功');
} catch (e) {
// 失败回滚勾选状态
try {
const res = await request.get(`/sys/worktype/${workTypeId}/roles`);
boundRoleIds.value = (res.data || []).map((x) => String(x.roleId));
} catch (err) {}
}
}
onMounted(() => {
loadRegistryOptions();
loadList();
});
</script>
<template>
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>工种管理</span>
<el-button type="primary" @click="openCreate">新增工种</el-button>
</div>
</template>
<div class="search-bar">
<el-input
v-model="query.keyword"
placeholder="工种名称 / 业务类型 / 生产成本分类"
clearable
style="width: 280px"
@keyup.enter="handleSearch"
/>
<el-button type="primary" @click="handleSearch">查询</el-button>
</div>
<el-table v-loading="loading" :data="list" border stripe>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="workName" label="工种名称" min-width="140" fixed="left" />
<el-table-column prop="bizType" label="业务类型" width="100" />
<el-table-column prop="departmentNames" label="所属部门" min-width="220" show-overflow-tooltip />
<el-table-column prop="sccbFenlei" label="生产成本分类" width="120" />
<el-table-column prop="sortOrder" label="排序" width="80" />
<el-table-column label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.isActive ? 'success' : 'danger'">{{ row.isActive ? '启用' : '禁用' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="340" fixed="right">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="openDeptDrawer(row)">绑定部门</el-button>
<el-button size="small" type="primary" link @click="openRoleDrawer(row)">关联角色</el-button>
<el-button size="small" type="primary" link @click="openEdit(row)">编辑</el-button>
<el-button size="small" :type="row.isActive ? 'warning' : 'success'" link @click="handleActive(row)">
{{ row.isActive ? '禁用' : '启用' }}
</el-button>
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="query.pageIndex"
v-model:page-size="query.pageSize"
:total="total"
layout="total, prev, pager, next"
class="pager"
@current-change="loadList"
/>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="460px">
<el-form :model="form" label-width="110px">
<el-form-item label="工种名称" required>
<el-input v-model="form.workName" placeholder="请输入工种名称" />
</el-form-item>
<el-form-item label="业务类型" required>
<el-select v-model="form.bizType" filterable allow-create default-first-option style="width: 100%">
<el-option v-for="v in bizTypeOptions" :key="v" :label="v" :value="v" />
</el-select>
</el-form-item>
<el-form-item label="生产成本分类" required>
<el-select v-model="form.sccbFenlei" filterable allow-create default-first-option style="width: 100%">
<el-option v-for="v in sccbFenleiOptions" :key="v" :label="v" :value="v" />
</el-select>
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="form.sortOrder" :min="0" :step="10" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave">确定</el-button>
</template>
</el-dialog>
<el-drawer v-model="deptDrawerVisible" :title="`绑定部门 - ${currentRow?.workName || ''}`" size="420px">
<el-alert type="info" :closable="false" show-icon title="勾选或取消勾选即时保存" style="margin-bottom: 12px" />
<el-tree
ref="deptTreeRef"
:data="deptTree"
show-checkbox
check-strictly
node-key="id"
default-expand-all
:props="{ label: 'name', children: 'children' }"
@check="handleDeptCheck"
/>
</el-drawer>
<el-drawer v-model="roleDrawerVisible" :title="`关联角色 - ${currentRow?.workName || ''}`" size="420px">
<el-alert type="info" :closable="false" show-icon title="勾选或取消勾选即时保存" style="margin-bottom: 12px" />
<el-checkbox-group v-model="boundRoleIds" @change="handleRoleChange">
<el-checkbox v-for="r in roleList" :key="r.id" :value="String(r.id)" class="role-item">
{{ r.name }}
</el-checkbox>
</el-checkbox-group>
</el-drawer>
</el-card>
</template>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.search-bar {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.pager {
margin-top: 12px;
justify-content: flex-end;
}
.role-item {
display: block;
margin: 6px 0;
}
</style>
+530
View File
@@ -0,0 +1,530 @@
<script setup>
import { ref, reactive, nextTick, onMounted } from 'vue';
import request from '../../../utils/request';
import { ElMessage, ElMessageBox } from 'element-plus';
// 枚举(与后端 WuziBusiness_Type_Enum / Department_DutyType_Enum 一致)
const businessTypeOptions = [
{ label: '原料', value: 1 },
{ label: '中间品', value: 2 },
{ label: '生丝', value: 3 },
{ label: '副产品', value: 4 },
{ label: '五金', value: 5 }
];
const departmentTypeOptions = [
{ label: '其他', value: 5 },
{ label: '仓储', value: 10 },
{ label: '选剥茧', value: 15 },
{ label: '煮茧', value: 20 },
{ label: '前缫', value: 25 },
{ label: '后缫', value: 30 },
{ label: '设备办', value: 35 },
{ label: '副产品', value: 40 },
{ label: '质检', value: 45 },
{ label: '丝绵', value: 50 },
{ label: '人力资源', value: 55 },
{ label: '丝业中心', value: 100 },
{ label: '前缫管理', value: 105 },
{ label: '后缫检验', value: 110 },
{ label: '后缫整理', value: 115 }
];
const groups = ref([]);
const treeData = ref([]);
const treeRef = ref(null);
const currentGroup = ref(null);
const groupLoading = ref(false);
const list = ref([]);
const total = ref(0);
const loading = ref(false);
const query = reactive({ keyword: '', pageIndex: 1, pageSize: 20 });
// 分组编辑弹窗
const groupDialogVisible = ref(false);
const groupDialogTitle = ref('新增一级分类');
const isGroupEdit = ref(false);
const groupForm = reactive({
id: null,
parentId: 0,
code: '',
name: '',
businessType: 1,
departmentType: 10,
sortOrder: 1,
description: ''
});
// 物资编辑弹窗
const muluDialogVisible = ref(false);
const muluDialogTitle = ref('新增物资目录');
const isMuluEdit = ref(false);
const muluForm = reactive({
id: null,
groupId: null,
code: '',
name: '',
unit: '',
guige: '',
xinghao: '',
pinyinName: '',
sortOrder: 1,
isYujing: false,
kucunyujing: 0,
saleRate: 0
});
function buildTree(items) {
const map = {};
const roots = [];
(items || []).forEach((g) => {
map[g.id] = { id: g.id, label: `${g.name}(${g.code})`, raw: g, children: [] };
});
(items || []).forEach((g) => {
const node = map[g.id];
if (g.parentId && map[g.parentId]) map[g.parentId].children.push(node);
else roots.push(node);
});
return roots;
}
async function loadGroups() {
groupLoading.value = true;
try {
const res = await request.get('/sys/wuzigroup');
groups.value = res.data || [];
treeData.value = buildTree(groups.value);
await nextTick();
if (!currentGroup.value && groups.value.length) {
selectGroupById(groups.value[0].id);
} else if (currentGroup.value) {
treeRef.value?.setCurrentKey(currentGroup.value.id);
}
} finally {
groupLoading.value = false;
}
}
function selectGroupById(id) {
const hit = groups.value.find((x) => x.id === id);
if (!hit) return;
currentGroup.value = hit;
treeRef.value?.setCurrentKey(id);
query.pageIndex = 1;
loadMulu();
}
function handleNodeClick(node) {
currentGroup.value = node.raw;
query.pageIndex = 1;
loadMulu();
}
async function loadMulu() {
loading.value = true;
try {
const res = await request.get('/sys/wuzimulu', {
params: {
groupId: currentGroup.value?.id || 0,
keyword: query.keyword || undefined,
pageIndex: query.pageIndex,
pageSize: query.pageSize
}
});
list.value = res.data.items || [];
total.value = res.data.total || 0;
} finally {
loading.value = false;
}
}
function handleSearch() {
query.pageIndex = 1;
loadMulu();
}
/* ==================== 物资分组 ==================== */
async function openGroupCreate(parentId) {
const parent = parentId ? groups.value.find((x) => x.id === parentId) : null;
const code = await request
.get('/sys/wuzigroup/new-code', { params: { parentId: parentId || 0 } })
.then((res) => res.data)
.catch(() => '');
isGroupEdit.value = false;
groupDialogTitle.value = parentId ? `新增「${parent?.name}」子级` : '新增一级分类';
Object.assign(groupForm, {
id: null,
parentId: parentId || 0,
code,
name: '',
businessType: parent ? parent.businessType : 1,
departmentType: parent ? parent.departmentType : 10,
sortOrder: Number(code) || 1,
description: ''
});
groupDialogVisible.value = true;
}
async function openGroupEdit() {
if (!currentGroup.value) {
ElMessage.warning('请选择需修改的分组');
return;
}
const res = await request.get(`/sys/wuzigroup/${currentGroup.value.id}`);
const g = res.data;
isGroupEdit.value = true;
groupDialogTitle.value = `修改「${g.name}」`;
Object.assign(groupForm, {
id: g.id,
parentId: g.parentId,
code: g.code,
name: g.name,
businessType: g.businessType,
departmentType: g.departmentType,
sortOrder: g.sortOrder,
description: g.description || ''
});
groupDialogVisible.value = true;
}
async function saveGroup() {
if (!groupForm.name) {
ElMessage.warning('请填写分组名称');
return;
}
try {
if (isGroupEdit.value) {
await request.put(`/sys/wuzigroup/${groupForm.id}`, {
businessType: groupForm.businessType,
departmentType: groupForm.departmentType,
name: groupForm.name,
sortOrder: Number(groupForm.sortOrder) || 0,
description: groupForm.description
});
ElMessage.success('修改成功');
} else {
await request.post('/sys/wuzigroup', {
parentId: groupForm.parentId,
businessType: groupForm.businessType,
departmentType: groupForm.departmentType,
code: groupForm.code,
name: groupForm.name,
sortOrder: Number(groupForm.sortOrder) || 0,
description: groupForm.description
});
ElMessage.success('新增成功');
}
groupDialogVisible.value = false;
await loadGroups();
} catch (e) {}
}
async function deleteGroup() {
if (!currentGroup.value) {
ElMessage.warning('请选择需删除的分组');
return;
}
const group = currentGroup.value;
await ElMessageBox.confirm(`确定删除分组「${group.name}」吗?`, '提示', { type: 'warning' });
try {
await request.delete(`/sys/wuzigroup/${group.id}`);
ElMessage.success('删除成功');
currentGroup.value = null;
await loadGroups();
if (!currentGroup.value && groups.value.length) selectGroupById(groups.value[0].id);
} catch (e) {}
}
/* ==================== 物资目录 ==================== */
async function openMuluCreate() {
if (!currentGroup.value) {
ElMessage.warning('请先选择分组');
return;
}
const code = await request
.get('/sys/wuzimulu/new-code', { params: { groupId: currentGroup.value.id } })
.then((res) => res.data)
.catch(() => '');
isMuluEdit.value = false;
muluDialogTitle.value = '新增物资目录';
Object.assign(muluForm, {
id: null,
groupId: currentGroup.value.id,
code,
name: '',
unit: '',
guige: '',
xinghao: '',
pinyinName: '',
sortOrder: Number(code) || 1,
isYujing: false,
kucunyujing: 0,
saleRate: 0
});
muluDialogVisible.value = true;
}
async function openMuluEdit(row) {
const res = await request.get(`/sys/wuzimulu/${row.id}`);
const m = res.data;
isMuluEdit.value = true;
muluDialogTitle.value = `修改物资「${m.name}」`;
Object.assign(muluForm, {
id: m.id,
groupId: m.groupId,
code: m.code,
name: m.name,
unit: m.unit || '',
guige: m.guige || '',
xinghao: m.xinghao || '',
pinyinName: m.pinyinName || '',
sortOrder: m.sortOrder,
isYujing: m.isYujing,
kucunyujing: m.kucunyujing,
saleRate: m.saleRate
});
muluDialogVisible.value = true;
}
async function saveMulu() {
if (!muluForm.name) {
ElMessage.warning('请填写物资名称');
return;
}
const payload = {
groupId: muluForm.groupId,
name: muluForm.name,
code: muluForm.code,
unit: muluForm.unit,
guige: muluForm.guige,
xinghao: muluForm.xinghao,
pinyinName: muluForm.pinyinName,
kucunyujing: Number(muluForm.kucunyujing) || 0,
sortOrder: Number(muluForm.sortOrder) || 0,
isYujing: muluForm.isYujing,
saleRate: Number(muluForm.saleRate) || 0
};
try {
if (isMuluEdit.value) {
await request.put(`/sys/wuzimulu/${muluForm.id}`, payload);
ElMessage.success('修改成功');
} else {
await request.post('/sys/wuzimulu', payload);
ElMessage.success('新增成功');
}
muluDialogVisible.value = false;
loadMulu();
} catch (e) {}
}
async function deleteMulu(row) {
await ElMessageBox.confirm(`确定删除物资「${row.name}」吗?`, '提示', { type: 'warning' });
try {
await request.delete(`/sys/wuzimulu/${row.id}`);
ElMessage.success('删除成功');
loadMulu();
} catch (e) {}
}
onMounted(loadGroups);
</script>
<template>
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>物资目录</span>
<div>
<el-button @click="openGroupCreate(0)">新增一级分类</el-button>
<el-button :disabled="!currentGroup" @click="openGroupCreate(currentGroup?.id)">新增子级</el-button>
<el-button :disabled="!currentGroup" @click="openGroupEdit">修改分组</el-button>
<el-button :disabled="!currentGroup" type="danger" @click="deleteGroup">删除分组</el-button>
</div>
</div>
</template>
<div class="wuzi-body">
<div class="tree-panel" v-loading="groupLoading">
<div class="tree-title">物资分组</div>
<el-tree
ref="treeRef"
:data="treeData"
node-key="id"
highlight-current
default-expand-all
:props="{ label: 'label', children: 'children' }"
@node-click="handleNodeClick"
/>
</div>
<div class="table-panel">
<div class="search-bar">
<span class="group-tip">当前分组:{{ currentGroup ? `${currentGroup.name}(${currentGroup.code})` : '未选择' }}</span>
<el-input
v-model="query.keyword"
placeholder="名称 / 编号 / 拼音码"
clearable
style="width: 220px"
@keyup.enter="handleSearch"
@clear="handleSearch"
/>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button type="primary" :disabled="!currentGroup" @click="openMuluCreate">新增物资</el-button>
</div>
<el-table v-loading="loading" :data="list" border stripe>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="name" label="名称" min-width="160" />
<el-table-column prop="code" label="编号" width="100" />
<el-table-column prop="unit" label="单位" width="70" />
<el-table-column prop="guige" label="规格" min-width="110" show-overflow-tooltip />
<el-table-column prop="xinghao" label="型号" min-width="110" show-overflow-tooltip />
<el-table-column prop="pinyinName" label="拼音码" width="90" />
<el-table-column prop="saleRate" label="销售税率" width="90" />
<el-table-column prop="sortOrder" label="顺序号" width="80" />
<el-table-column label="启用预警" width="90">
<template #default="{ row }">
<el-tag :type="row.isYujing ? 'success' : 'info'">{{ row.isYujing ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="kucunyujing" label="预警数量" width="90" />
<el-table-column label="操作" width="140" fixed="right">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="openMuluEdit(row)">编辑</el-button>
<el-button size="small" type="danger" link @click="deleteMulu(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="query.pageIndex"
v-model:page-size="query.pageSize"
:total="total"
layout="total, prev, pager, next"
class="pager"
@current-change="loadMulu"
/>
</div>
</div>
<!-- 分组编辑 -->
<el-dialog v-model="groupDialogVisible" :title="groupDialogTitle" width="480px">
<el-form :model="groupForm" label-width="110px">
<el-form-item label="编号">
<el-input v-model="groupForm.code" readonly placeholder="自动生成" />
</el-form-item>
<el-form-item label="名称" required>
<el-input v-model="groupForm.name" placeholder="分组名称" />
</el-form-item>
<el-form-item label="业务类型">
<el-select v-model="groupForm.businessType" style="width: 100%">
<el-option v-for="b in businessTypeOptions" :key="b.value" :label="b.label" :value="b.value" />
</el-select>
</el-form-item>
<el-form-item label="部门职责">
<el-select v-model="groupForm.departmentType" filterable style="width: 100%">
<el-option v-for="d in departmentTypeOptions" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
</el-form-item>
<el-form-item label="顺序号">
<el-input-number v-model="groupForm.sortOrder" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item label="描述">
<el-input v-model="groupForm.description" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="groupDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveGroup">确定</el-button>
</template>
</el-dialog>
<!-- 物资编辑 -->
<el-dialog v-model="muluDialogVisible" :title="muluDialogTitle" width="560px">
<el-form :model="muluForm" label-width="100px">
<el-form-item label="所属分组">
<el-select v-model="muluForm.groupId" filterable style="width: 100%">
<el-option v-for="g in groups" :key="g.id" :label="`${g.name}(${g.code})`" :value="g.id" />
</el-select>
</el-form-item>
<el-form-item label="编号">
<el-input v-model="muluForm.code" readonly placeholder="按分组自动生成" />
</el-form-item>
<el-form-item label="名称" required>
<el-input v-model="muluForm.name" placeholder="物资名称" />
</el-form-item>
<el-form-item label="单位">
<el-input v-model="muluForm.unit" placeholder="如 kg / 个 / 包" />
</el-form-item>
<el-form-item label="规格">
<el-input v-model="muluForm.guige" />
</el-form-item>
<el-form-item label="型号">
<el-input v-model="muluForm.xinghao" />
</el-form-item>
<el-form-item label="拼音码">
<el-input v-model="muluForm.pinyinName" />
</el-form-item>
<el-form-item label="顺序号">
<el-input-number v-model="muluForm.sortOrder" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item label="销售税率">
<el-input-number v-model="muluForm.saleRate" :min="0" :precision="2" style="width: 100%" />
</el-form-item>
<el-form-item label="启用预警">
<el-switch v-model="muluForm.isYujing" />
</el-form-item>
<el-form-item label="预警数量">
<el-input-number v-model="muluForm.kucunyujing" :min="0" :precision="2" style="width: 100%" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="muluDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveMulu">确定</el-button>
</template>
</el-dialog>
</el-card>
</template>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.wuzi-body {
display: flex;
gap: 12px;
}
.tree-panel {
width: 250px;
flex-shrink: 0;
border-right: 1px solid var(--el-border-color-lighter);
padding-right: 8px;
max-height: 640px;
overflow: auto;
}
.tree-title {
font-weight: 600;
margin-bottom: 8px;
}
.table-panel {
flex: 1;
min-width: 0;
}
.search-bar {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 12px;
}
.group-tip {
color: var(--el-text-color-secondary);
font-size: 12px;
flex: 1;
}
.pager {
margin-top: 12px;
justify-content: flex-end;
}
</style>
@@ -0,0 +1,406 @@
<script setup>
import { ref, reactive, onMounted } from 'vue';
import request from '../../../utils/request';
import { ElMessage, ElMessageBox } from 'element-plus';
const list = ref([]);
const total = ref(0);
const loading = ref(false);
const query = reactive({
year: String(new Date().getFullYear()),
chandi: '',
zhandian: '',
jibie: '',
like: '',
pageIndex: 1,
pageSize: 20
});
// 注册表「基础数据\原料庄口」提供的下拉选项(允许手工录入新值)
const registryOptions = reactive({ chandi: [], zhandian: [], jibie: [], canpinzhong: [] });
const wuziOptions = ref([]);
const bendiwaidiOptions = [
{ label: '本地', value: 1 },
{ label: '外地', value: 2 }
];
const dialogVisible = ref(false);
const dialogTitle = ref('新增原料庄口');
const isEdit = ref(false);
const editId = ref(null);
const nameAuto = ref(true);
const form = reactive({
year: String(new Date().getFullYear()),
name: '',
jibie: '',
bendiwaidi: 1,
chandi: '',
canpinzhong: '',
wzmlId: null,
zhandian: '',
pici: 0
});
const digits = ['', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
function bendiwaidiLabel(v) {
const hit = bendiwaidiOptions.find((x) => x.value === Number(v));
return hit ? hit.label : v;
}
async function loadRegistryOptions() {
const load = async (keyName) => {
const res = await request.get('/sys/registry/by-key', {
params: { category: '基础数据\\原料庄口', keyName }
});
return (res.data?.value || '')
.split(',')
.map((s) => s.trim())
.filter(Boolean);
};
try {
const [chandi, zhandian, jibie, canpinzhong] = await Promise.all([
load('产地'),
load('站点'),
load('季别'),
load('蚕品种')
]);
registryOptions.chandi = chandi;
registryOptions.zhandian = zhandian;
registryOptions.jibie = jibie;
registryOptions.canpinzhong = canpinzhong;
} catch (e) {
// 注册表未配置时下拉为空,仍可手工录入
}
}
async function loadWuziOptions() {
try {
const res = await request.get('/sys/yuanliaozhuangkou/wuzi-options');
wuziOptions.value = res.data || [];
} catch (e) {}
}
async function loadList() {
loading.value = true;
try {
const res = await request.get('/sys/yuanliaozhuangkou', {
params: {
year: query.year || 0,
chandi: query.chandi || undefined,
zhandian: query.zhandian || undefined,
jibie: query.jibie || undefined,
like: query.like || undefined,
pageIndex: query.pageIndex,
pageSize: query.pageSize
}
});
list.value = res.data.items || [];
total.value = res.data.total || 0;
} finally {
loading.value = false;
}
}
function handleSearch() {
query.pageIndex = 1;
loadList();
}
// 按 "{年度}{站点}{季别}{批次中文}{物资名去茧}({蚕品种})" 生成庄口名称
function calcName() {
if (!nameAuto.value) return;
const yearStr = form.year || '';
const zhandianStr = (form.zhandian || '').trim();
const jibieStr = (form.jibie || '').trim();
const pici = Number(form.pici) || 0;
const piciStr = pici < digits.length ? digits[pici] : String(pici);
const wuzi = wuziOptions.value.find((x) => x.id === form.wzmlId);
const wzmlName = (wuzi?.name || '').replace(/茧/g, '');
const canpinzhongStr = (form.canpinzhong || '').trim();
if (!yearStr && !zhandianStr && !jibieStr && !wzmlName) return;
form.name = `${yearStr}${zhandianStr}${jibieStr}${piciStr}${wzmlName}(${canpinzhongStr})`;
}
function openCreate() {
isEdit.value = false;
nameAuto.value = true;
dialogTitle.value = '新增原料庄口';
Object.assign(form, {
year: String(new Date().getFullYear()),
name: '',
jibie: '',
bendiwaidi: 1,
chandi: '',
canpinzhong: '',
wzmlId: wuziOptions.value.length === 1 ? wuziOptions.value[0].id : null,
zhandian: '',
pici: 0
});
dialogVisible.value = true;
calcName();
}
function openEdit(row) {
isEdit.value = true;
nameAuto.value = false;
editId.value = row.id;
dialogTitle.value = '修改原料庄口';
Object.assign(form, {
year: String(row.year),
name: row.name,
jibie: row.jibie,
bendiwaidi: Number(row.bendiwaidi),
chandi: row.chandi,
canpinzhong: row.canpinzhong,
wzmlId: row.wzmlId,
zhandian: row.zhandian,
pici: row.pici
});
dialogVisible.value = true;
}
async function handleSave() {
if (!form.name) {
ElMessage.warning('请填写庄口名称');
return;
}
if (!form.chandi || !form.zhandian || !form.jibie || !form.canpinzhong) {
ElMessage.warning('请填写产地、站点、季别、蚕品种');
return;
}
if (!form.wzmlId) {
ElMessage.warning('请选择茧别(物资)');
return;
}
const payload = {
year: Number(form.year),
name: form.name,
jibie: form.jibie,
bendiWaidi: form.bendiwaidi,
chandi: form.chandi,
canpinzhong: form.canpinzhong,
wzmlId: form.wzmlId,
zhandian: form.zhandian,
pici: Number(form.pici) || 0
};
try {
if (isEdit.value) {
await request.put(`/sys/yuanliaozhuangkou/${editId.value}`, payload);
ElMessage.success('修改成功');
} else {
await request.post('/sys/yuanliaozhuangkou', payload);
ElMessage.success('新增成功');
}
dialogVisible.value = false;
loadList();
} catch (e) {}
}
async function handleDelete(row) {
await ElMessageBox.confirm(`确定删除原料庄口「${row.name}」吗?`, '提示', { type: 'warning' });
try {
await request.delete(`/sys/yuanliaozhuangkou/${row.id}`);
ElMessage.success('删除成功');
loadList();
} catch (e) {}
}
onMounted(async () => {
await Promise.all([loadRegistryOptions(), loadWuziOptions()]);
loadList();
});
</script>
<template>
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>原料庄口</span>
<el-button type="primary" @click="openCreate">新增原料庄口</el-button>
</div>
</template>
<div class="search-bar">
<el-date-picker
v-model="query.year"
type="year"
placeholder="年度"
value-format="YYYY"
style="width: 130px"
@change="handleSearch"
/>
<el-select v-model="query.chandi" placeholder="产地" clearable filterable style="width: 130px" @change="handleSearch">
<el-option v-for="v in registryOptions.chandi" :key="v" :label="v" :value="v" />
</el-select>
<el-select v-model="query.zhandian" placeholder="站点" clearable filterable style="width: 130px" @change="handleSearch">
<el-option v-for="v in registryOptions.zhandian" :key="v" :label="v" :value="v" />
</el-select>
<el-select v-model="query.jibie" placeholder="季别" clearable filterable style="width: 120px" @change="handleSearch">
<el-option v-for="v in registryOptions.jibie" :key="v" :label="v" :value="v" />
</el-select>
<el-input
v-model="query.like"
placeholder="名称 / 编号 / 蚕品种"
clearable
style="width: 200px"
@keyup.enter="handleSearch"
@clear="handleSearch"
/>
<el-button type="primary" @click="handleSearch">查询</el-button>
</div>
<el-table v-loading="loading" :data="list" border stripe>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="year" label="年度" width="80" />
<el-table-column prop="name" label="庄口名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="code" label="编号" width="100" />
<el-table-column prop="wzmlName" label="茧别" width="90" />
<el-table-column label="本地外地" width="90">
<template #default="{ row }">{{ bendiwaidiLabel(row.bendiwaidi) }}</template>
</el-table-column>
<el-table-column prop="chandi" label="产地" width="100" />
<el-table-column prop="jibie" label="季别" width="90" />
<el-table-column prop="canpinzhong" label="蚕品种" width="110" />
<el-table-column prop="pici" label="批次" width="70" />
<el-table-column prop="zhandian" label="站点" width="110" />
<el-table-column label="操作" width="140" fixed="right">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="openEdit(row)">编辑</el-button>
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="query.pageIndex"
v-model:page-size="query.pageSize"
:total="total"
layout="total, prev, pager, next"
class="pager"
@current-change="loadList"
/>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="560px">
<el-form :model="form" label-width="100px">
<el-form-item label="年度" required>
<el-date-picker
v-model="form.year"
type="year"
placeholder="年度"
value-format="YYYY"
style="width: 100%"
@change="calcName"
/>
</el-form-item>
<el-form-item label="站点" required>
<el-select
v-model="form.zhandian"
filterable
allow-create
default-first-option
placeholder="站点"
style="width: 100%"
@change="calcName"
>
<el-option v-for="v in registryOptions.zhandian" :key="v" :label="v" :value="v" />
</el-select>
</el-form-item>
<el-form-item label="季别" required>
<el-select
v-model="form.jibie"
filterable
allow-create
default-first-option
placeholder="季别"
style="width: 100%"
@change="calcName"
>
<el-option v-for="v in registryOptions.jibie" :key="v" :label="v" :value="v" />
</el-select>
</el-form-item>
<el-form-item label="批次">
<el-input-number v-model="form.pici" :min="0" :max="9" style="width: 100%" @change="calcName" />
</el-form-item>
<el-form-item label="茧别" required>
<el-select
v-model="form.wzmlId"
filterable
placeholder="选择物资目录中的茧类物资"
style="width: 100%"
@change="calcName"
>
<el-option
v-for="w in wuziOptions"
:key="w.id"
:label="w.code ? `${w.name}(${w.code})` : w.name"
:value="w.id"
/>
</el-select>
</el-form-item>
<el-form-item label="蚕品种" required>
<el-select
v-model="form.canpinzhong"
filterable
allow-create
default-first-option
placeholder="蚕品种"
style="width: 100%"
@change="calcName"
>
<el-option v-for="v in registryOptions.canpinzhong" :key="v" :label="v" :value="v" />
</el-select>
</el-form-item>
<el-form-item label="产地" required>
<el-select
v-model="form.chandi"
filterable
allow-create
default-first-option
placeholder="产地"
style="width: 100%"
>
<el-option v-for="v in registryOptions.chandi" :key="v" :label="v" :value="v" />
</el-select>
</el-form-item>
<el-form-item label="本地外地" required>
<el-select v-model="form.bendiwaidi" style="width: 100%">
<el-option v-for="b in bendiwaidiOptions" :key="b.value" :label="b.label" :value="b.value" />
</el-select>
</el-form-item>
<el-form-item label="庄口名称" required>
<el-input v-model="form.name" placeholder="自动生成,可手工修改" @input="nameAuto = false" />
<div class="form-tip">按「年度+站点+季别+批次+茧别(蚕品种)」自动生成</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave">确定</el-button>
</template>
</el-dialog>
</el-card>
</template>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.search-bar {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.pager {
margin-top: 12px;
justify-content: flex-end;
}
.form-tip {
color: var(--el-text-color-secondary);
font-size: 12px;
line-height: 1.4;
}
</style>
+256
View File
@@ -0,0 +1,256 @@
<script setup>
import { ref, reactive, onMounted } from 'vue';
import request from '../../../utils/request';
import { ElMessage, ElMessageBox } from 'element-plus';
const list = ref([]);
const total = ref(0);
const loading = ref(false);
const query = reactive({
year: String(new Date().getFullYear()),
keyword: '',
pageIndex: 1,
pageSize: 20
});
const monthOptions = Array.from({ length: 12 }, (_, i) => i + 1);
// 新增/编辑
const dialogVisible = ref(false);
const dialogTitle = ref('新增账期');
const isEdit = ref(false);
const editId = ref(null);
const form = reactive({
year: String(new Date().getFullYear()),
month: new Date().getMonth() + 1,
startDate: '',
endDate: ''
});
function fmtDate(v) {
return v ? String(v).slice(0, 10) : '';
}
// 按 28 日边界推算默认账期区间(与 F9 一致)
function defaultRange() {
const now = new Date();
let start;
let end;
if (now.getDate() <= 28) {
start = new Date(now.getFullYear(), now.getMonth() - 1, 29);
end = new Date(now.getFullYear(), now.getMonth(), 28);
} else {
start = new Date(now.getFullYear(), now.getMonth(), 29);
end = new Date(now.getFullYear(), now.getMonth() + 1, 28);
}
const pad = (n) => String(n).padStart(2, '0');
const str = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
return { start: str(start), end: str(end) };
}
async function loadList() {
loading.value = true;
try {
const res = await request.get('/sys/zhangqi', {
params: {
year: query.year || undefined,
keyword: query.keyword || undefined,
pageIndex: query.pageIndex,
pageSize: query.pageSize
}
});
list.value = res.data.items || [];
total.value = res.data.total || 0;
} finally {
loading.value = false;
}
}
function handleSearch() {
query.pageIndex = 1;
loadList();
}
function openCreate() {
const range = defaultRange();
isEdit.value = false;
dialogTitle.value = '新增账期';
Object.assign(form, {
year: String(new Date().getFullYear()),
month: new Date().getMonth() + 1,
startDate: range.start,
endDate: range.end
});
dialogVisible.value = true;
}
function openEdit(row) {
isEdit.value = true;
editId.value = row.id;
dialogTitle.value = '修改账期';
Object.assign(form, {
year: String(row.year),
month: row.month,
startDate: fmtDate(row.startDate),
endDate: fmtDate(row.endDate)
});
dialogVisible.value = true;
}
async function handleSave() {
if (!form.startDate || !form.endDate) {
ElMessage.warning('请选择开始日期与截止日期');
return;
}
if (form.startDate > form.endDate) {
ElMessage.warning('开始日期不能大于截止日期');
return;
}
const payload = {
year: Number(form.year),
month: Number(form.month),
startDate: `${form.startDate}T00:00:00`,
endDate: `${form.endDate}T23:59:59`
};
try {
if (isEdit.value) {
await request.put(`/sys/zhangqi/${editId.value}`, payload);
ElMessage.success('修改成功');
} else {
await request.post('/sys/zhangqi', payload);
ElMessage.success('新增成功');
}
dialogVisible.value = false;
loadList();
} catch (e) {}
}
async function handleDelete(row) {
await ElMessageBox.confirm(`确定删除 ${row.year} 年 ${row.month} 月账期吗?`, '提示', { type: 'warning' });
try {
await request.delete(`/sys/zhangqi/${row.id}`);
ElMessage.success('删除成功');
loadList();
} catch (e) {}
}
async function handleZhaZhang(row) {
const isZhaZhang = !row.isZhaZhang;
const tip = isZhaZhang
? '扎帐后,相关数据会禁止修改,是否继续?'
: '取消扎帐后,相关数据允许修改,是否继续?';
await ElMessageBox.confirm(tip, '提示', { type: 'warning' });
try {
await request.put(`/sys/zhangqi/${row.id}/zhazhang`, { isZhaZhang });
ElMessage.success(isZhaZhang ? '扎帐成功' : '已取消扎帐');
loadList();
} catch (e) {}
}
onMounted(loadList);
</script>
<template>
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>账期管理</span>
<el-button type="primary" @click="openCreate">新增账期</el-button>
</div>
</template>
<div class="search-bar">
<el-date-picker
v-model="query.year"
type="year"
placeholder="年度"
value-format="YYYY"
style="width: 140px"
@change="handleSearch"
/>
<el-input
v-model="query.keyword"
placeholder="月份 / 年度"
clearable
style="width: 160px"
@keyup.enter="handleSearch"
/>
<el-button type="primary" @click="handleSearch">查询</el-button>
</div>
<el-table v-loading="loading" :data="list" border stripe>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="year" label="年度" width="100" />
<el-table-column prop="month" label="月份" width="100" />
<el-table-column label="开始日期" width="140">
<template #default="{ row }">{{ fmtDate(row.startDate) }}</template>
</el-table-column>
<el-table-column label="截止日期" width="140">
<template #default="{ row }">{{ fmtDate(row.endDate) }}</template>
</el-table-column>
<el-table-column label="是否扎帐" min-width="120">
<template #default="{ row }">
<el-tag v-if="row.isZhaZhang" type="success">已扎帐</el-tag>
<el-tag v-else type="info">未扎帐</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-button v-if="!row.isZhaZhang" size="small" type="primary" link @click="handleZhaZhang(row)">扎帐</el-button>
<el-button v-else size="small" type="warning" link @click="handleZhaZhang(row)">取消扎帐</el-button>
<el-button size="small" type="primary" link :disabled="row.isZhaZhang" @click="openEdit(row)">编辑</el-button>
<el-button size="small" type="danger" link :disabled="row.isZhaZhang" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="query.pageIndex"
v-model:page-size="query.pageSize"
:total="total"
layout="total, prev, pager, next"
class="pager"
@current-change="loadList"
/>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="480px">
<el-form :model="form" label-width="100px">
<el-form-item label="年度" required>
<el-date-picker v-model="form.year" type="year" placeholder="年度" value-format="YYYY" style="width: 100%" />
</el-form-item>
<el-form-item label="月份" required>
<el-select v-model="form.month" placeholder="月份" style="width: 100%">
<el-option v-for="m in monthOptions" :key="m" :label="`${m} 月`" :value="m" />
</el-select>
</el-form-item>
<el-form-item label="开始日期" required>
<el-date-picker v-model="form.startDate" type="date" placeholder="开始日期" value-format="YYYY-MM-DD" style="width: 100%" />
</el-form-item>
<el-form-item label="截止日期" required>
<el-date-picker v-model="form.endDate" type="date" placeholder="截止日期" value-format="YYYY-MM-DD" style="width: 100%" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave">确定</el-button>
</template>
</el-dialog>
</el-card>
</template>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.search-bar {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.pager {
margin-top: 12px;
justify-content: flex-end;
}
</style>