Initial commit

This commit is contained in:
南宁网宿科技
2023-06-03 23:57:39 +08:00
commit 13bed2bafb
1017 changed files with 175796 additions and 0 deletions
@@ -0,0 +1,396 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
width="75%"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑配件' : '添加配件'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<div style="background-color: #f3f3f3; padding: 8px">
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ md: { span: 7 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 17 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<a-card title="基本信息" :bordered="false">
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="配件分类" name="accessoryCategory">
<category-select
:data="categoryList"
placeholder="请选择商品分类"
v-model:value="form.accessoryCategory"
/>
</a-form-item>
<a-form-item label="配件规格" name="accessorySpecs">
<a-input
allow-clear
placeholder="请输入配件规格"
v-model:value="form.accessorySpecs"
/>
</a-form-item>
<a-form-item label="重量" name="accessoryWeight">
<a-input
allow-clear
placeholder="请输入配件规格"
v-model:value="form.accessoryWeight"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="配件名称" name="accessoryName">
<a-input
allow-clear
placeholder="请输入配件名称"
v-model:value="form.accessoryName"
/>
</a-form-item>
<a-form-item label="单位" name="accessoryUnit">
<a-input
allow-clear
placeholder="请输入配件单位"
v-model:value="form.accessoryUnit"
/>
</a-form-item>
<a-form-item label="单价" name="accessoryPrice">
<a-input-number
allow-clear
:min="0"
:precision="2"
style="width: 200px"
placeholder="请输入配件单价"
v-model:value="form.accessoryPrice"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="设备型号" name="accessoryModel">
<model-select
:data="modelTree"
placeholder="请选择设备型号"
v-model:value="form.accessoryModel"
@change="handleModel"
/>
</a-form-item>
<a-form-item label="使用年限" name="lifeYear">
<a-input
allow-clear
placeholder="请输入使用年限"
v-model:value="form.lifeYear"
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
<a-divider style="height: 8px" />
<a-card title="出厂信息" :bordered="false">
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="配件编号" name="accessoryNo">
<a-input
allow-clear
placeholder="请输入当前位置"
v-model:value="form.accessoryNo"
/>
</a-form-item>
<a-form-item label="产权单位" name="companyName">
<TowerCompany
:placeholder="`请选择租赁单位`"
v-model:value="form.companyName"
:customer-type="`产权单位`"
@done="chooseCompanyName"
/>
<!-- <DictSelect-->
<!-- dict-code="propertyCompany"-->
<!-- placeholder="请选择产权单位"-->
<!-- v-model:value="form.companyName"-->
<!-- />-->
</a-form-item>
<a-form-item label="备注" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.comments"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="制造厂商" name="manufactor">
<a-input
allow-clear
placeholder="请输入制造厂商"
v-model:value="form.manufactor"
/>
</a-form-item>
<a-form-item label="厂商编号" name="manufactorNo">
<a-input
allow-clear
placeholder="请输入厂商编号"
v-model:value="form.manufactorNo"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="出厂日期" name="factoryDate">
<a-date-picker
class="ele-fluid"
value-format="YYYY-MM-DD"
placeholder="请选择日期"
v-model:value="form.factoryDate"
/>
</a-form-item>
<a-form-item label="报废日期" name="scrapDate">
<a-date-picker
class="ele-fluid"
value-format="YYYY-MM-DD"
placeholder="请选择日期"
v-model:value="form.scrapDate"
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
<a-divider style="height: 8px" />
<a-card title="多选适配设备型号" :bordered="false">
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="设备型号" name="accessoryModel">
<SelectWarehouse
:placeholder="`请选择设备型号`"
v-model:value="form.accessoryModel"
@done="chooseModel"
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
</a-form>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import { addAccessory, updateAccessory } from '@/api/tower/accessory';
import { Accessory } from '@/api/tower/accessory/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import CategorySelect from './category-select.vue';
import ModelSelect from './model-select.vue';
import { FormInstance } from 'ant-design-vue/es/form';
import { Warehouse } from '@/api/tower/warehouse/model';
import { Customer } from '@/api/oa/customer/model';
import { Category } from '@/api/goods/category/model';
import { TowerModel } from '@/api/tower/model/model';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Accessory | null;
categoryList?: Category[];
modelTree?: TowerModel[];
modelList?: TowerModel[];
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
const formRef = ref<FormInstance | null>(null);
// 用户信息
const form = reactive<Accessory>({
accessoryId: undefined,
accessoryName: '',
accessoryNo: '',
accessoryCategory: '',
accessoryModel: '',
accessorySpecs: '',
accessoryPrice: 0,
accessoryUnit: '',
accessoryStock: '',
companyName: '',
manufactor: '',
accessoryWeight: '',
manufactorNo: '',
lifeYear: undefined,
factoryDate: '',
scrapDate: '',
status: undefined,
comments: '',
sortNumber: undefined,
userId: undefined,
createTime: '',
updateTime: ''
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
accessoryCategory: [
{
required: true,
type: 'number',
message: '请输入配件分类',
trigger: 'blur'
}
],
accessoryName: [
{
required: true,
type: 'string',
message: '请输入配件名称',
trigger: 'blur'
}
],
accessoryModel: [
{
required: true,
type: 'number',
message: '请输入设备型号',
trigger: 'blur'
}
],
accessoryWeight: [
{
required: true,
type: 'string',
message: '请输入设备重量',
trigger: 'blur'
}
],
accessorySpecs: [
{
required: true,
type: 'string',
message: '请输入设备规格',
trigger: 'blur'
}
],
accessoryUnit: [
{
required: true,
type: 'string',
message: '请输入单位',
trigger: 'blur'
}
]
});
const { resetFields } = useForm(form, rules);
const chooseModel = (data: Warehouse) => {
form.accessoryModel = data.warehouseName;
};
const chooseCompanyName = (data: Customer) => {
form.companyName = data.customerName;
};
const handleModel = (index) => {
props.modelList?.map((d) => {
console.log(index);
console.log(d.categoryId);
if (index == d.categoryId) {
form.lifeYear = Number(d.yearLife);
}
});
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateAccessory : addAccessory;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>
<style lang="less"></style>
<style lang="less">
.tab-pane {
min-height: 300px;
}
.ml-10 {
margin-left: 5px;
}
.upload-text {
margin-right: 70px;
}
</style>
@@ -0,0 +1,39 @@
<!-- 目录选择下拉框 -->
<template>
<a-tree-select
allow-clear
tree-default-expand-all
:placeholder="placeholder"
:value="value || undefined"
:tree-data="data"
:dropdown-style="{ maxHeight: '360px', overflow: 'auto' }"
@update:value="updateValue"
/>
</template>
<script lang="ts" setup>
import type { Goods } from '@/api/goods/model';
const emit = defineEmits<{
(e: 'update:value', value?: number): void;
}>();
withDefaults(
defineProps<{
// 选中的数据(v-modal)
value?: number;
// 提示信息
placeholder?: string;
// 文章分类
data: Goods[];
}>(),
{
placeholder: '请选择上级分类'
}
);
/* 更新选中数据 */
const updateValue = (value?: number) => {
emit('update:value', value);
};
</script>
@@ -0,0 +1,105 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
:width="600"
:visible="visible"
:maskClosable="false"
:title="`导入设备`"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
:footer="null"
>
<a-upload-dragger
name="file"
:show-upload-list="false"
accept=".xls,.xlsx"
:before-upload="handleImport"
>
<p class="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p class="ant-upload-text">单击或拖动文件到此区域进行上传</p>
<p class="ant-upload-hint" style="max-width: 550px">
提示:附件上传中,请不要做任何操作! 支持的文件类型为*.xls, *.xlsx!
</p>
</a-upload-dragger>
<div style="margin: 20px auto; display: flex; justify-content: center">
<a
href="https://file.wsdns.cn/download/20230523/8b20591126ab49448fdd69cfee8ab260.xlsx"
target="_blank"
>下载设备导入模板</a
>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { InboxOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import { read, utils } from 'xlsx';
import { ref } from 'vue';
import { Accessory } from '@/api/tower/accessory/model';
import { addBatchAccessory } from '@/api/tower/accessory';
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 导入的数据
const importData = ref<Accessory[]>([]);
const handleImport = (file: File) => {
if (
![
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
].includes(file.type)
) {
message.error('只能选择 excel 文件');
return false;
}
if (file.size / 1024 / 1024 > 20) {
message.error('大小不能超过 20MB');
return false;
}
const reader = new FileReader();
reader.onload = (e) => {
const data = new Uint8Array(e.target?.result as any);
const workbook = read(data, { type: 'array' });
const sheetNames = workbook.SheetNames;
const worksheet = workbook.Sheets[sheetNames[0]];
// 解析成二维数组
const aoa = utils.sheet_to_json<string[]>(worksheet, { header: 1 });
console.log(aoa);
aoa.slice(1).map((d) => {
importData.value.push({
accessoryName: d[0],
accessoryCategory: d[1],
accessoryModel: d[2],
accessorySpecs: d[3],
accessoryUnit: d[4],
accessoryWeight: d[5],
accessoryPrice: Number(d[6]),
accessoryNo: d[7],
companyNo: d[8],
companyName: d[9],
manufactor: d[10],
factoryDate: d[11],
comments: d[12]
});
});
addBatchAccessory(importData.value).then(() => {
message.success('导入成功');
updateVisible(false);
emit('done');
});
};
reader.readAsArrayBuffer(file);
return false;
};
</script>
@@ -0,0 +1,39 @@
<!-- 目录选择下拉框 -->
<template>
<a-tree-select
allow-clear
tree-default-expand-all
:placeholder="placeholder"
:value="value || undefined"
:tree-data="data"
:dropdown-style="{ maxHeight: '360px', overflow: 'auto' }"
@update:value="updateValue"
/>
</template>
<script lang="ts" setup>
import type { Goods } from '@/api/goods/model';
const emit = defineEmits<{
(e: 'update:value', value?: number): void;
}>();
withDefaults(
defineProps<{
// 选中的数据(v-modal)
value?: number;
// 提示信息
placeholder?: string;
// 文章分类
data: Goods[];
}>(),
{
placeholder: '请选择上级分类'
}
);
/* 更新选中数据 */
const updateValue = (value?: number) => {
emit('update:value', value);
};
</script>
@@ -0,0 +1,177 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加配件</span>
</a-button>
<a-button class="ele-btn-icon" @click="openImport">
<template #icon>
<UploadOutlined />
</template>
<span>导入配件</span>
</a-button>
<a-button class="ele-btn-icon" @click="handleExport">
<template #icon>
<DownloadOutlined />
</template>
<span>导出配件</span>
</a-button>
<a-button
danger
type="primary"
class="ele-btn-icon"
v-if="selection.length > 0"
@click="removeBatch"
>
<template #icon>
<DeleteOutlined />
</template>
<span>批量删除</span>
</a-button>
<a-button> 批量生成二维码 </a-button>
<a-button> 打印配件码 </a-button>
<a-input-search
allow-clear
placeholder="请输入关键词"
v-model:value="searchText"
@pressEnter="search"
@search="search"
/>
<!-- 导入弹窗 -->
<Import v-model:visible="showImport" @done="search" />
</a-space>
</template>
<script lang="ts" setup>
import {
PlusOutlined,
UploadOutlined,
DownloadOutlined,
DeleteOutlined
} from '@ant-design/icons-vue';
import useSearch from '@/utils/use-search';
import type { AccessoryParam } from '@/api/tower/accessory/model';
import { ref, watch } from 'vue';
import { utils, writeFile } from 'xlsx';
import { message } from 'ant-design-vue/es';
import { Accessory } from '@/api/tower/accessory/model';
import Import from './import.vue';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: AccessoryParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 表单数据
const { where } = useSearch<AccessoryParam>({
accessoryId: undefined,
accessoryName: '',
status: undefined,
keywords: ''
});
// 下来选项
// 搜索内容
const searchText = ref('');
// 是否显示导入弹窗
const showImport = ref(false);
/* 搜索 */
const search = () => {
where.keywords = searchText.value;
emit('search', where);
};
// 新增
const add = () => {
emit('add');
};
// 导出
const handleExport = () => {
if (!props.selection?.length) {
message.error('请至少选择一条数据');
return;
}
const array: (string | number)[][] = [
[
'配件名称',
'配件分类',
'适用设备型号',
'配件规格',
'配件单位',
'重量',
'单价',
'自编号',
'产权单位编码',
'产权单位名称',
'制造厂家',
'出厂日期',
'备注'
]
];
props.selection?.forEach((d: Accessory) => {
array.push([
`${d.accessoryName}`,
`${d.accessoryCategory}`,
`${d.accessoryModel}`,
`${d.accessorySpecs}`,
`${d.accessoryUnit}`,
`${d.accessoryWeight}`,
`${d.accessoryPrice}`,
`${d.accessoryNo}`,
`${d.companyNo}`,
`${d.companyName}`,
`${d.manufactor}`,
`${d.factoryDate}`,
`${d.comments}`
]);
});
const sheetName = '配件列表';
const workbook = {
SheetNames: [sheetName],
Sheets: {}
};
const sheet = utils.aoa_to_sheet(array);
workbook.Sheets[sheetName] = sheet;
// 设置列宽
sheet['!cols'] = [
{ wch: 10 },
{ wch: 10 },
{ wch: 10 },
{ wch: 10 },
{ wch: 20 },
{ wch: 40 },
{ wch: 10 }
];
writeFile(workbook, '导出配件管理表.xlsx');
};
// 批量删除
const removeBatch = () => {
emit('remove');
};
// 导入
const openImport = () => {
showImport.value = true;
};
watch(
() => props.selection,
() => {}
);
</script>
+442
View File
@@ -0,0 +1,442 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="accessoryId"
:columns="columns"
:datasource="datasource"
v-model:selection="selection"
:customRow="customRow"
tool-class="ele-toolbar-form"
:scroll="{ x: 3000 }"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'accessoryName'">
<a-avatar
:size="30"
:src="`${record.accessoryAvatar}`"
style="margin-right: 4px"
:srcset="`https://file.wsdns.cn/${record.accessoryAvatar}`"
>
<template #icon>
<UserOutlined />
</template>
</a-avatar>
<a-tooltip title="查看详情">
<a href="#" @click="openInfo(record)">{{
record.accessoryName
}}</a>
</a-tooltip>
</template>
<template v-if="column.key === 'progress'">
<div v-for="(d, i) in progressDict" :key="i">
<span v-if="d.value === record.progress">{{ d.label }}</span>
</div>
</template>
<template v-if="column.key === 'accessoryType'">
<div v-for="(d, i) in JSON.parse(accessoryType)" :key="i">
<span v-if="d.value === record.accessoryType">{{
d.value
}}</span>
</div>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">正常</a-tag>
<a-tag v-if="record.status === 1" color="red">待审核</a-tag>
<a-tag v-if="record.status === 2" color="purple">已驳回</a-tag>
</template>
<template v-if="column.key === 'nickname'">
<a-tooltip :title="`${record.nickname}`">
<a-avatar :src="record.userAvatar" size="small" />
</a-tooltip>
</template>
<template v-if="column.key === 'createTime'">
<a-tooltip :title="`${toDateString(record.createTime)}`">
{{ timeAgo(record.createTime) }}
</a-tooltip>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a>库存分布</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<AccessoryEdit
v-model:visible="showEdit"
:data="current"
:category-list="data"
:model-tree="data2"
:model-list="data2List"
@done="reload"
/>
<!-- 批量转移弹窗 -->
<!-- <AccessoryMove v-model:visible="showMove" :data="selection" @done="batchMove" />-->
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import {
ExclamationCircleOutlined,
UserOutlined
} from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { toDateString, toTreeData } from 'ele-admin-pro';
import Search from './components/search.vue';
import AccessoryEdit from './components/accessory-edit.vue';
import {
pageAccessory,
removeAccessory,
removeBatchAccessory
} from '@/api/tower/accessory';
import { timeAgo } from 'ele-admin-pro';
import type { Accessory, AccessoryParam } from '@/api/tower/accessory/model';
import { useUserStore } from '@/store/modules/user';
import { Category } from '@/api/goods/category/model';
import { listCategory } from '@/api/goods/category';
import { listTowerModel } from '@/api/tower/model';
import { TowerModel } from '@/api/tower/model/model';
const userStore = useUserStore();
// 当前用户信息
const loginUser = computed(() => userStore.info ?? {});
const accessoryType = localStorage.getItem('accessoryType');
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<Accessory[]>([]);
// 当前编辑数据
const current = ref<Accessory | null>(null);
// 树形数据
const data = ref<Category[]>([]);
const data2 = ref<TowerModel[]>([]);
const data2List = ref<TowerModel[]>([]);
// 是否显示资产详情
const showInfo = ref(false);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 树展开的key
const expandedRowKeys = ref<number[]>([]);
// 树选中的key
const selectedRowKeys = ref<number[]>([]);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.progress = filters.progress;
where.accessorySource = filters.accessorySource;
where.accessoryType = filters.accessoryType;
where.status = filters.status;
}
return pageAccessory({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '配件编号',
dataIndex: 'accessoryNo'
},
{
title: '配件名称',
dataIndex: 'accessoryName'
},
{
title: '配件分类',
dataIndex: 'accessoryCategory'
},
{
title: '适用设备型号',
dataIndex: 'accessoryModel'
},
{
title: '配件规格',
dataIndex: 'accessorySpecs'
},
{
title: '单价',
dataIndex: 'accessoryPrice'
},
{
title: '配件单位',
dataIndex: 'accessoryUnit'
},
{
title: '库存总量',
dataIndex: 'accessoryStock'
},
{
title: '产权单位名称',
dataIndex: 'companyName'
},
{
title: '制造厂商',
dataIndex: 'manufactor'
},
{
title: '制造厂商编号',
dataIndex: 'manufactorNo'
},
{
title: '使用年限',
dataIndex: 'lifeYear'
},
{
title: '出厂日期',
dataIndex: 'factoryDate',
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '报废日期',
dataIndex: 'scrapDate',
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '操作',
key: 'action',
width: 200,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: AccessoryParam) => {
console.log(where);
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: Accessory) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 打开用户详情弹窗 */
const openInfo = (row?: Accessory) => {
current.value = row ?? null;
showInfo.value = true;
};
/* 删除单个 */
const remove = (row: Accessory) => {
const hide = message.loading('请求中..', 0);
removeAccessory(row.accessoryId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量转移 */
const batchMove = (userId) => {
console.log(userId, '批量转移0000');
console.log(selection.value);
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchAccessory(
selection.value.map((d) => {
if (loginUser.value.userId === d.userId) {
return d.accessoryId;
}
})
)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
listCategory()
.then((list) => {
loading.value = false;
const eks: number[] = [];
list.forEach((d) => {
d.key = d.categoryId;
d.value = d.categoryId;
if (typeof d.categoryId === 'number') {
eks.push(d.categoryId);
}
});
expandedRowKeys.value = eks;
data.value = toTreeData({
data: list,
idField: 'categoryId',
parentIdField: 'parentId'
});
if (list.length) {
if (typeof list[0].categoryId === 'number') {
selectedRowKeys.value = [list[0].categoryId];
}
} else {
selectedRowKeys.value = [];
}
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
};
/* 查询 */
const query2 = () => {
loading.value = true;
listTowerModel()
.then((list) => {
loading.value = false;
data2List.value = list;
const eks: number[] = [];
list.forEach((d) => {
d.key = d.categoryId;
d.value = d.categoryId;
if (typeof d.categoryId === 'number') {
eks.push(d.categoryId);
}
});
expandedRowKeys.value = eks;
data2.value = toTreeData({
data: list,
idField: 'categoryId',
parentIdField: 'parentId'
});
if (list.length) {
if (typeof list[0].categoryId === 'number') {
selectedRowKeys.value = [list[0].categoryId];
}
} else {
selectedRowKeys.value = [];
}
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
};
/* 自定义行属性 */
const customRow = (record: Accessory) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
query2();
</script>
<script lang="ts">
export default {
name: 'Accessory'
};
</script>
<style lang="less" scoped>
.sys-org-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.sys-org-table :deep(.ant-table-pagination.ant-pagination) {
padding: 0 4px;
margin-bottom: 0;
}
</style>
@@ -0,0 +1,266 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
:width="750"
:visible="visible"
:confirm-loading="loading"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑合同' : '添加合同'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
:label-col="{ md: { span: 6 }, sm: { span: 20 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 24 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<a-row :gutter="16">
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="合同名称" v-bind="validateInfos.customerName">
<a-input
allow-clear
:maxlength="30"
placeholder="请输入合同名称"
v-model:value="form.customerName"
@blur="
validate('customerName', { trigger: 'blur' }).catch(() => {})
"
/>
</a-form-item>
<a-form-item label="合同标识" v-bind="validateInfos.customerCode">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入社会统一信用代码"
v-model:value="form.customerCode"
/>
</a-form-item>
<a-form-item label="联系人" v-bind="validateInfos.customerContacts">
<a-input
allow-clear
:maxlength="20"
placeholder="请填写联系人"
v-model:value="form.customerContacts"
/>
</a-form-item>
<a-form-item label="手机号码" v-bind="validateInfos.customerMobile">
<a-input
allow-clear
:maxlength="20"
placeholder="请填写联系人手机号码"
v-model:value="form.customerMobile"
/>
</a-form-item>
<a-form-item label="头像" v-bind="validateInfos.customerAvatar">
<ele-image-upload
v-model:value="images"
:item-style="{ width: '90px', height: '90px' }"
:limit="1"
@upload="onUpload"
/>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="合同全称" v-bind="validateInfos.customerFullName">
<a-input
allow-clear
:maxlength="30"
placeholder="请输入合同全称"
v-model:value="form.customerFullName"
@blur="
validate('customerFullName', { trigger: 'blur' }).catch(() => {})
"
/>
</a-form-item>
<a-form-item
label="联系地址"
v-bind="validateInfos.customerAddress"
>
<a-input
allow-clear
placeholder="请填写联系地址"
v-model:value="form.customerAddress"
/>
</a-form-item>
<a-form-item label="公司座机" v-bind="validateInfos.customerPhone">
<a-input
allow-clear
:maxlength="20"
placeholder="请填写公司座机电话"
v-model:value="form.customerPhone"
/>
</a-form-item>
<a-form-item label="排序" v-bind="validateInfos.sortNumber">
<a-input
allow-clear
:maxlength="20"
placeholder="排序"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="备注" v-bind="validateInfos.comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.comments"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import {ref, reactive, watch, computed} from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import { addCustomer, updateCustomer } from '@/api/oa/customer';
import type { Customer } from '@/api/oa/customer/model';
import { createCode } from '@/utils/common';
import { uploadFile } from '@/api/system/file';
import type { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FILE_SERVER } from '@/config/setting';
import { useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
// 当前用户信息
const loginUser = computed(() => userStore.info ?? {});
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Customer | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 用户信息
const form = reactive<Customer>({
customerCode: '',
customerName: '',
customerFullName: '',
customerType: undefined,
progress: undefined,
customerMobile: '',
customerAvatar: '',
customerPhone: '',
customerSource: '',
customerContacts: '',
customerAddress: '',
comments: '',
status: '0',
sortNumber: 100,
customerId: 0,
userId: '',
});
// 已上传数据, 可赋初始值用于回显
const images = ref(<any>[]);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
customerName: [
{
required: true,
type: 'string',
message: '请输入合同名称',
trigger: 'blur'
}
],
customerCode: [
{
required: true,
type: 'string',
message: '请输入合法的IP地址',
trigger: 'blur'
}
]
});
const { resetFields, validate, validateInfos } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
validate()
.then(() => {
loading.value = true;
// 去除空格
form.customerName = form.customerName?.replace(/\s*/g, '');
// 判断权限
// if (loginUser.value.roles?.[0].roleCode != 'admin'){
// form.status = '1';
// }
const data = {
...form
};
// 转字符串
const saveOrUpdate = isUpdate.value ? updateCustomer : addCustomer;
saveOrUpdate(data)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
// 上传文件
const onUpload = (d: ItemType) => {
uploadFile(<File>d.file)
.then((result) => {
form.customerAvatar = result.path;
message.success('上传成功');
})
.catch((e) => {
message.error(e.message);
});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
loading.value = false;
// 头像赋值
images.value = [];
if(props.data.customerAvatar){
images.value.push({ uid:1, url: FILE_SERVER + props.data.customerAvatar, status: '' });
}
assignObject(form, props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>
<style lang="less"></style>
@@ -0,0 +1,148 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
width="75%"
:visible="visible"
:confirm-loading="loading"
:title="'合同详情'"
:maxable="true"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
:footer="null"
>
<a-form
:label-col="{ md: { span: 4 }, sm: { span: 24 } }"
:wrapper-col="{ md: { span: 19 }, sm: { span: 24 } }"
>
<div class="base-form" style="margin-bottom: 20px">
<a-descriptions bordered>
<a-descriptions-item label="合同名称">
{{ customer.customerName }}
</a-descriptions-item>
<a-descriptions-item label="社会统一信用代码">
{{ customer.customerCode }}
</a-descriptions-item>
<a-descriptions-item label="跟进状态">
<div color="blue" v-for="(d, index) in progress" :key="index">
<span v-if="d.value == customer.progress">{{ d.label }}</span>
</div>
</a-descriptions-item>
<a-descriptions-item label="联系人">
{{ customer.customerContacts }}
</a-descriptions-item>
<a-descriptions-item label="联系电话">
{{ customer.customerMobile }}
</a-descriptions-item>
<a-descriptions-item label="座机电话">
{{ customer.customerPhone }}
</a-descriptions-item>
<a-descriptions-item label="合同类型">
<div color="blue" v-for="(d, index) in customerType" :key="index">
<span v-if="d.value == customer.customerType">{{ d.value }}</span>
</div>
</a-descriptions-item>
<a-descriptions-item label="联系地址">
{{ customer.customerAddress }}
</a-descriptions-item>
<a-descriptions-item label="备注">
{{ customer.comments }}
</a-descriptions-item>
</a-descriptions>
<!-- <a-descriptions-->
<!-- title="其他信息"-->
<!-- :column="1"-->
<!-- bordered-->
<!-- style="margin-top: 30px"-->
<!-- >-->
<!-- <a-descriptions-item label="相关项目">-->
<!-- {{ customer.comments }}-->
<!-- </a-descriptions-item>-->
<!-- </a-descriptions>-->
</div>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import type { Customer } from '@/api/oa/customer/model';
import { FILE_SERVER } from '@/config/setting';
import { getDictionaryOptions } from '@/utils/common';
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Customer | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 用户信息
const customer = reactive<Customer>({
customerCode: '',
customerName: '',
customerType: undefined,
customerMobile: '',
customerAvatar: '',
customerPhone: '',
customerContacts: '',
customerAddress: '',
comments: '',
progress: '',
status: '0',
sortNumber: 100,
customerId: 0
});
// 请求状态
const loading = ref(true);
const { resetFields } = useForm(customer);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
/* 打开外部链接 */
// const openUrl = (record) => {
// window.open(record.panel);
// };
/* 获取字典数据 */
const customerType = getDictionaryOptions('customerType');
const progress = getDictionaryOptions('customerFollowStatus');
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
loading.value = false;
assignObject(customer, props.data);
}
} else {
resetFields();
}
}
);
</script>
<style lang="less">
.tab-pane {
min-height: 100px;
}
.card-head {
display: flex;
height: 40px;
align-items: center;
margin-bottom: 30px;
}
</style>
@@ -0,0 +1,206 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>新增</span>
</a-button>
<a-button
danger
type="primary"
class="ele-btn-icon"
v-if="selection.length > 0"
@click="removeBatch"
>
<template #icon>
<DeleteOutlined />
</template>
<span>批量删除</span>
</a-button>
<!-- <a-button @click="batchMove" v-if="selection.length > 0">-->
<!-- <template #icon>-->
<!-- <UserSwitchOutlined />-->
<!-- </template>-->
<!-- 批量转移-->
<!-- </a-button>-->
<a-input-search
allow-clear
placeholder="请输入关键词"
v-model:value="searchText"
@pressEnter="search"
@search="search"
/>
</a-space>
</template>
<script lang="ts" setup>
import {
PlusOutlined,
DeleteOutlined,
UploadOutlined,
DownloadOutlined
} from '@ant-design/icons-vue';
import useSearch from '@/utils/use-search';
import type { CustomerParam } from '@/api/oa/customer/model';
import { ref, watch } from 'vue';
import { utils, read } from 'xlsx';
// import { assignObject } from 'ele-admin-pro';
import { message } from 'ant-design-vue/es';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: CustomerParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 表单数据
const { where } = useSearch<CustomerParam>({
customerName: '',
customerCode: '',
nickname: '',
keywords: '',
userId: undefined
});
// 下来选项
// 搜索内容
const searchText = ref('');
/* 搜索 */
const search = () => {
where.keywords = searchText.value;
emit('search', where);
};
// 新增
const add = () => {
emit('add');
};
// 导入数据的列
const importTitle = ref<string[]>(['A', 'B', 'C', 'D', 'E', 'F', 'G']);
// 导入的数据
const importData = ref<Record<string, any>[]>([]);
// 导入数据二维数组形式
const importDataAoa = ref<(string | number)[][]>([]);
/* 导入本地 excel 文件 */
const importBatch = (file: File) => {
if (
![
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
].includes(file.type)
) {
message.error('只能选择 excel 文件');
return false;
}
if (file.size / 1024 / 1024 > 20) {
message.error('大小不能超过 20MB');
return false;
}
const reader = new FileReader();
reader.onload = (e) => {
const data = new Uint8Array(e.target?.result as any);
const workbook = read(data, { type: 'array' });
const sheetNames = workbook.SheetNames;
const worksheet = workbook.Sheets[sheetNames[0]];
// 解析成二维数组
const aoa = utils.sheet_to_json<string[]>(worksheet, { header: 1 });
// 生成表格需要的数据
let list: Record<string, any>[] = [];
let maxCols = 0;
let title: string[] = [];
aoa.forEach((d) => {
if (d.length > maxCols) {
maxCols = d.length;
}
const row = {};
for (let i = 0; i < d.length; i++) {
const key = getCharByIndex(i);
row[key] = d[i];
row['__colspan__' + key] = 1;
row['__rowspan__' + key] = 1;
}
list.push(row);
});
for (let i = 0; i < maxCols; i++) {
title.push(getCharByIndex(i));
}
importTitle.value = title;
importData.value = list;
importDataAoa.value = aoa;
};
console.log(importData.value);
console.log(importDataAoa.value);
reader.readAsArrayBuffer(file);
return false;
};
/* 生成Excel列字母序号 */
const getCharByIndex = (index: number) => {
const chars = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z'
];
if (index < chars.length) {
return chars[index];
}
const n = parseInt(String(index / chars.length));
const m = index % chars.length;
return chars[n] + chars[m];
};
// 转移
// const batchMove = () => {
// emit('batchMove');
// };
// 批量删除
const removeBatch = () => {
emit('remove');
};
const onClear = () => {
where.userId = undefined;
search();
};
watch(
() => props.selection,
() => {}
);
</script>
+306
View File
@@ -0,0 +1,306 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="customerId"
:columns="columns"
:datasource="datasource"
v-model:selection="selection"
tool-class="ele-toolbar-form"
:scroll="{ x: 800 }"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'customerName'">
<a-avatar
:size="30"
:src="`${record.customerAvatar}`"
style="margin-right: 4px"
:srcset="`https://file.wsdns.cn/${record.customerAvatar}`"
>
<template #icon>
<UserOutlined />
</template>
</a-avatar>
<a-tooltip title="查看详情">
<a href="#" @click="openInfo(record)">{{
record.customerName
}}</a>
</a-tooltip>
</template>
<template v-if="column.key === 'progress'">
<div v-for="(d, i) in progressDict" :key="i">
<span v-if="d.value === record.progress">{{ d.label }}</span>
</div>
</template>
<template v-if="column.key === 'customerType'">
<div v-for="(d, i) in JSON.parse(customerType)" :key="i">
<span v-if="d.value === record.customerType">{{
d.value
}}</span>
</div>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">正常</a-tag>
<a-tag v-if="record.status === 1" color="red">待审核</a-tag>
<a-tag v-if="record.status === 2" color="purple">已驳回</a-tag>
</template>
<template v-if="column.key === 'nickname'">
<a-tooltip :title="`${record.nickname}`">
<a-avatar :src="record.userAvatar" size="small" />
</a-tooltip>
</template>
<template v-if="column.key === 'createTime'">
<a-tooltip :title="`${toDateString(record.createTime)}`">
{{ timeAgo(record.createTime) }}
</a-tooltip>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<CustomerEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 合同详情弹窗 -->
<CustomerInfo v-model:visible="showInfo" :data="current" @done="reload" />
<!-- 批量转移弹窗 -->
<!-- <CustomerMove v-model:visible="showMove" :data="selection" @done="batchMove" />-->
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import {
ExclamationCircleOutlined,
UserOutlined
} from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { toDateString } from 'ele-admin-pro';
import Search from './components/search.vue';
import CustomerEdit from './components/customer-edit.vue';
import CustomerInfo from './components/customer-info.vue';
import {
pageCustomer,
removeCustomer,
removeBatchCustomer
} from '@/api/oa/customer';
import { timeAgo } from 'ele-admin-pro';
import type { Customer, CustomerParam } from '@/api/oa/customer/model';
import { useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
// 当前用户信息
const loginUser = computed(() => userStore.info ?? {});
const customerType = localStorage.getItem('customerType');
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<Customer[]>([]);
// 当前编辑数据
const current = ref<Customer | null>(null);
// 是否显示资产详情
const showInfo = ref(false);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.progress = filters.progress;
where.customerSource = filters.customerSource;
where.customerType = filters.customerType;
where.status = filters.status;
}
return pageCustomer({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '合同编号',
dataIndex: 'contractNo',
key: 'contractNo',
ellipsis: true
},
{
title: '项目名称',
dataIndex: 'customerContacts'
},
{
title: '预计产值',
dataIndex: 'customerContacts'
},
{
title: '结算产值',
dataIndex: 'customerContacts'
},
{
title: '业务联系人',
dataIndex: 'customerContacts'
},
{
title: '签订日期',
dataIndex: 'customerContacts'
},
{
title: '已生成结算次数',
dataIndex: 'customerContacts'
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: CustomerParam) => {
console.log(where);
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: Customer) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 打开用户详情弹窗 */
const openInfo = (row?: Customer) => {
current.value = row ?? null;
showInfo.value = true;
};
/* 删除单个 */
const remove = (row: Customer) => {
const hide = message.loading('请求中..', 0);
removeCustomer(row.customerId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量转移 */
const batchMove = (userId) => {
console.log(userId, '批量转移0000');
console.log(selection.value);
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchCustomer(
selection.value.map((d) => {
if (loginUser.value.userId === d.userId) {
return d.customerId;
}
})
)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
</script>
<script lang="ts">
export default {
name: 'Accessory'
};
</script>
<style lang="less" scoped>
.sys-org-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.sys-org-table :deep(.ant-table-pagination.ant-pagination) {
padding: 0 4px;
margin-bottom: 0;
}
</style>
@@ -0,0 +1,266 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
:width="750"
:visible="visible"
:confirm-loading="loading"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑客户' : '添加客户'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
:label-col="{ md: { span: 6 }, sm: { span: 20 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 24 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<a-row :gutter="16">
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="客户名称" v-bind="validateInfos.customerName">
<a-input
allow-clear
:maxlength="30"
placeholder="请输入客户名称"
v-model:value="form.customerName"
@blur="
validate('customerName', { trigger: 'blur' }).catch(() => {})
"
/>
</a-form-item>
<a-form-item label="客户标识" v-bind="validateInfos.customerCode">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入社会统一信用代码"
v-model:value="form.customerCode"
/>
</a-form-item>
<a-form-item label="联系人" v-bind="validateInfos.customerContacts">
<a-input
allow-clear
:maxlength="20"
placeholder="请填写联系人"
v-model:value="form.customerContacts"
/>
</a-form-item>
<a-form-item label="手机号码" v-bind="validateInfos.phone">
<a-input
allow-clear
:maxlength="20"
placeholder="请填写联系人手机号码"
v-model:value="form.phone"
/>
</a-form-item>
<a-form-item label="头像" v-bind="validateInfos.customerAvatar">
<ele-image-upload
v-model:value="images"
:item-style="{ width: '90px', height: '90px' }"
:limit="1"
@upload="onUpload"
/>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="客户全称" v-bind="validateInfos.customerFullName">
<a-input
allow-clear
:maxlength="30"
placeholder="请输入客户全称"
v-model:value="form.customerFullName"
@blur="
validate('customerFullName', { trigger: 'blur' }).catch(() => {})
"
/>
</a-form-item>
<a-form-item
label="联系地址"
v-bind="validateInfos.customerAddress"
>
<a-input
allow-clear
placeholder="请填写联系地址"
v-model:value="form.customerAddress"
/>
</a-form-item>
<a-form-item label="公司座机" v-bind="validateInfos.customerPhone">
<a-input
allow-clear
:maxlength="20"
placeholder="请填写公司座机电话"
v-model:value="form.customerPhone"
/>
</a-form-item>
<a-form-item label="排序" v-bind="validateInfos.sortNumber">
<a-input
allow-clear
:maxlength="20"
placeholder="排序"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="备注" v-bind="validateInfos.comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.comments"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import {ref, reactive, watch, computed} from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import { addCustomer, updateCustomer } from '@/api/oa/customer';
import type { Customer } from '@/api/oa/customer/model';
import { createCode } from '@/utils/common';
import { uploadFile } from '@/api/system/file';
import type { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FILE_SERVER } from '@/config/setting';
import { useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
// 当前用户信息
const loginUser = computed(() => userStore.info ?? {});
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Customer | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 用户信息
const form = reactive<Customer>({
customerCode: '',
customerName: '',
customerFullName: '',
customerType: undefined,
progress: undefined,
customerMobile: '',
customerAvatar: '',
customerPhone: '',
customerSource: '',
customerContacts: '',
customerAddress: '',
comments: '',
status: '0',
sortNumber: 100,
customerId: 0,
userId: '',
});
// 已上传数据, 可赋初始值用于回显
const images = ref(<any>[]);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
customerName: [
{
required: true,
type: 'string',
message: '请输入客户名称',
trigger: 'blur'
}
],
customerCode: [
{
required: true,
type: 'string',
message: '请输入合法的IP地址',
trigger: 'blur'
}
]
});
const { resetFields, validate, validateInfos } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
validate()
.then(() => {
loading.value = true;
// 去除空格
form.customerName = form.customerName?.replace(/\s*/g, '');
// 判断权限
// if (loginUser.value.roles?.[0].roleCode != 'admin'){
// form.status = '1';
// }
const data = {
...form
};
// 转字符串
const saveOrUpdate = isUpdate.value ? updateCustomer : addCustomer;
saveOrUpdate(data)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
// 上传文件
const onUpload = (d: ItemType) => {
uploadFile(<File>d.file)
.then((result) => {
form.customerAvatar = result.path;
message.success('上传成功');
})
.catch((e) => {
message.error(e.message);
});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
loading.value = false;
// 头像赋值
images.value = [];
if(props.data.customerAvatar){
images.value.push({ uid:1, url: FILE_SERVER + props.data.customerAvatar, status: '' });
}
assignObject(form, props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>
<style lang="less"></style>
@@ -0,0 +1,148 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
width="75%"
:visible="visible"
:confirm-loading="loading"
:title="'客户详情'"
:maxable="true"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
:footer="null"
>
<a-form
:label-col="{ md: { span: 4 }, sm: { span: 24 } }"
:wrapper-col="{ md: { span: 19 }, sm: { span: 24 } }"
>
<div class="base-form" style="margin-bottom: 20px">
<a-descriptions bordered>
<a-descriptions-item label="客户名称">
{{ customer.customerName }}
</a-descriptions-item>
<a-descriptions-item label="社会统一信用代码">
{{ customer.customerCode }}
</a-descriptions-item>
<a-descriptions-item label="跟进状态">
<div color="blue" v-for="(d, index) in progress" :key="index">
<span v-if="d.value == customer.progress">{{ d.label }}</span>
</div>
</a-descriptions-item>
<a-descriptions-item label="联系人">
{{ customer.customerContacts }}
</a-descriptions-item>
<a-descriptions-item label="联系电话">
{{ customer.customerMobile }}
</a-descriptions-item>
<a-descriptions-item label="座机电话">
{{ customer.customerPhone }}
</a-descriptions-item>
<a-descriptions-item label="客户类型">
<div color="blue" v-for="(d, index) in customerType" :key="index">
<span v-if="d.value == customer.customerType">{{ d.value }}</span>
</div>
</a-descriptions-item>
<a-descriptions-item label="联系地址">
{{ customer.customerAddress }}
</a-descriptions-item>
<a-descriptions-item label="备注">
{{ customer.comments }}
</a-descriptions-item>
</a-descriptions>
<!-- <a-descriptions-->
<!-- title="其他信息"-->
<!-- :column="1"-->
<!-- bordered-->
<!-- style="margin-top: 30px"-->
<!-- >-->
<!-- <a-descriptions-item label="相关项目">-->
<!-- {{ customer.comments }}-->
<!-- </a-descriptions-item>-->
<!-- </a-descriptions>-->
</div>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import type { Customer } from '@/api/oa/customer/model';
import { FILE_SERVER } from '@/config/setting';
import { getDictionaryOptions } from '@/utils/common';
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Customer | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 用户信息
const customer = reactive<Customer>({
customerCode: '',
customerName: '',
customerType: undefined,
customerMobile: '',
customerAvatar: '',
customerPhone: '',
customerContacts: '',
customerAddress: '',
comments: '',
progress: '',
status: '0',
sortNumber: 100,
customerId: 0
});
// 请求状态
const loading = ref(true);
const { resetFields } = useForm(customer);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
/* 打开外部链接 */
// const openUrl = (record) => {
// window.open(record.panel);
// };
/* 获取字典数据 */
const customerType = getDictionaryOptions('customerType');
const progress = getDictionaryOptions('customerFollowStatus');
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
loading.value = false;
assignObject(customer, props.data);
}
} else {
resetFields();
}
}
);
</script>
<style lang="less">
.tab-pane {
min-height: 100px;
}
.card-head {
display: flex;
height: 40px;
align-items: center;
margin-bottom: 30px;
}
</style>
@@ -0,0 +1,223 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加客户</span>
</a-button>
<a-upload
:before-upload="importBatch"
:show-upload-list="false"
accept=".xls,.xlsx"
>
<a-button class="ele-btn-icon"> <DownloadOutlined />批量导入 </a-button>
</a-upload>
<a-button class="ele-btn-icon">
<span>下载客户模板</span>
</a-button>
<a-upload
:before-upload="importBatch"
:show-upload-list="false"
accept=".xls,.xlsx"
>
<a-button class="ele-btn-icon"> <UploadOutlined />导出 </a-button>
</a-upload>
<a-button
danger
type="primary"
class="ele-btn-icon"
v-if="selection.length > 0"
@click="removeBatch"
>
<template #icon>
<DeleteOutlined />
</template>
<span>批量删除</span>
</a-button>
<!-- <a-button @click="batchMove" v-if="selection.length > 0">-->
<!-- <template #icon>-->
<!-- <UserSwitchOutlined />-->
<!-- </template>-->
<!-- 批量转移-->
<!-- </a-button>-->
<a-input-search
allow-clear
placeholder="请输入关键词"
v-model:value="searchText"
@pressEnter="search"
@search="search"
/>
</a-space>
</template>
<script lang="ts" setup>
import {
PlusOutlined,
DeleteOutlined,
UploadOutlined,
DownloadOutlined
} from '@ant-design/icons-vue';
import useSearch from '@/utils/use-search';
import type { CustomerParam } from '@/api/oa/customer/model';
import { ref, watch } from 'vue';
import { utils, read } from 'xlsx';
// import { assignObject } from 'ele-admin-pro';
import { message } from 'ant-design-vue/es';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: CustomerParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 表单数据
const { where } = useSearch<CustomerParam>({
customerName: '',
customerCode: '',
nickname: '',
keywords: '',
userId: undefined
});
// 下来选项
// 搜索内容
const searchText = ref('');
/* 搜索 */
const search = () => {
where.keywords = searchText.value;
emit('search', where);
};
// 新增
const add = () => {
emit('add');
};
// 导入数据的列
const importTitle = ref<string[]>(['A', 'B', 'C', 'D', 'E', 'F', 'G']);
// 导入的数据
const importData = ref<Record<string, any>[]>([]);
// 导入数据二维数组形式
const importDataAoa = ref<(string | number)[][]>([]);
/* 导入本地 excel 文件 */
const importBatch = (file: File) => {
if (
![
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
].includes(file.type)
) {
message.error('只能选择 excel 文件');
return false;
}
if (file.size / 1024 / 1024 > 20) {
message.error('大小不能超过 20MB');
return false;
}
const reader = new FileReader();
reader.onload = (e) => {
const data = new Uint8Array(e.target?.result as any);
const workbook = read(data, { type: 'array' });
const sheetNames = workbook.SheetNames;
const worksheet = workbook.Sheets[sheetNames[0]];
// 解析成二维数组
const aoa = utils.sheet_to_json<string[]>(worksheet, { header: 1 });
// 生成表格需要的数据
let list: Record<string, any>[] = [];
let maxCols = 0;
let title: string[] = [];
aoa.forEach((d) => {
if (d.length > maxCols) {
maxCols = d.length;
}
const row = {};
for (let i = 0; i < d.length; i++) {
const key = getCharByIndex(i);
row[key] = d[i];
row['__colspan__' + key] = 1;
row['__rowspan__' + key] = 1;
}
list.push(row);
});
for (let i = 0; i < maxCols; i++) {
title.push(getCharByIndex(i));
}
importTitle.value = title;
importData.value = list;
importDataAoa.value = aoa;
};
console.log(importData.value);
console.log(importDataAoa.value);
reader.readAsArrayBuffer(file);
return false;
};
/* 生成Excel列字母序号 */
const getCharByIndex = (index: number) => {
const chars = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z'
];
if (index < chars.length) {
return chars[index];
}
const n = parseInt(String(index / chars.length));
const m = index % chars.length;
return chars[n] + chars[m];
};
// 转移
// const batchMove = () => {
// emit('batchMove');
// };
// 批量删除
const removeBatch = () => {
emit('remove');
};
const onClear = () => {
where.userId = undefined;
search();
};
watch(
() => props.selection,
() => {}
);
</script>
+291
View File
@@ -0,0 +1,291 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="customerId"
:columns="columns"
:datasource="datasource"
v-model:selection="selection"
tool-class="ele-toolbar-form"
:scroll="{ x: 800 }"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'customerName'">
<a-avatar
:size="30"
:src="`${record.customerAvatar}`"
style="margin-right: 4px"
:srcset="`https://file.wsdns.cn/${record.customerAvatar}`"
>
<template #icon>
<UserOutlined />
</template>
</a-avatar>
<a-tooltip title="查看详情">
<a href="#" @click="openInfo(record)">{{
record.customerName
}}</a>
</a-tooltip>
</template>
<template v-if="column.key === 'progress'">
<div v-for="(d, i) in progressDict" :key="i">
<span v-if="d.value === record.progress">{{ d.label }}</span>
</div>
</template>
<template v-if="column.key === 'customerType'">
<div v-for="(d, i) in JSON.parse(customerType)" :key="i">
<span v-if="d.value === record.customerType">{{
d.value
}}</span>
</div>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">正常</a-tag>
<a-tag v-if="record.status === 1" color="red">待审核</a-tag>
<a-tag v-if="record.status === 2" color="purple">已驳回</a-tag>
</template>
<template v-if="column.key === 'nickname'">
<a-tooltip :title="`${record.nickname}`">
<a-avatar :src="record.userAvatar" size="small" />
</a-tooltip>
</template>
<template v-if="column.key === 'createTime'">
<a-tooltip :title="`${toDateString(record.createTime)}`">
{{ timeAgo(record.createTime) }}
</a-tooltip>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<CustomerEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 客户详情弹窗 -->
<CustomerInfo v-model:visible="showInfo" :data="current" @done="reload" />
<!-- 批量转移弹窗 -->
<!-- <CustomerMove v-model:visible="showMove" :data="selection" @done="batchMove" />-->
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import {
ExclamationCircleOutlined,
UserOutlined
} from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { toDateString } from 'ele-admin-pro';
import Search from './components/search.vue';
import CustomerEdit from './components/customer-edit.vue';
import CustomerInfo from './components/customer-info.vue';
import {
pageCustomer,
removeCustomer,
removeBatchCustomer
} from '@/api/oa/customer';
import { timeAgo } from 'ele-admin-pro';
import type { Customer, CustomerParam } from '@/api/oa/customer/model';
import { useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
// 当前用户信息
const loginUser = computed(() => userStore.info ?? {});
const customerType = localStorage.getItem('customerType');
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<Customer[]>([]);
// 当前编辑数据
const current = ref<Customer | null>(null);
// 是否显示资产详情
const showInfo = ref(false);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.progress = filters.progress;
where.customerSource = filters.customerSource;
where.customerType = filters.customerType;
where.status = filters.status;
}
return pageCustomer({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '客户名称',
dataIndex: 'customerName',
key: 'customerName',
width: 280,
ellipsis: true
},
{
title: '联系人',
dataIndex: 'customerContacts'
},
{
title: '联系电话',
dataIndex: 'customerMobile'
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: CustomerParam) => {
console.log(where);
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: Customer) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 打开用户详情弹窗 */
const openInfo = (row?: Customer) => {
current.value = row ?? null;
showInfo.value = true;
};
/* 删除单个 */
const remove = (row: Customer) => {
const hide = message.loading('请求中..', 0);
removeCustomer(row.customerId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量转移 */
const batchMove = (userId) => {
console.log(userId, '批量转移0000');
console.log(selection.value);
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchCustomer(
selection.value.map((d) => {
if (loginUser.value.userId === d.userId) {
return d.customerId;
}
})
)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
</script>
<script lang="ts">
export default {
name: 'Accessory'
};
</script>
<style lang="less" scoped>
.sys-org-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.sys-org-table :deep(.ant-table-pagination.ant-pagination) {
padding: 0 4px;
margin-bottom: 0;
}
</style>
@@ -0,0 +1,300 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="740"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '修改分类' : '新建分类'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 6, sm: 4, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 18, sm: 20, xs: 24 } : { flex: '1' }
"
>
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="上级分类" name="parentId">
<a-tree-select
allow-clear
:tree-data="categoryList"
tree-default-expand-all
placeholder="请选择上级分类"
:value="form.parentId || undefined"
:dropdown-style="{ maxHeight: '360px', overflow: 'auto' }"
@update:value="(value?: number) => (form.parentId = value)"
/>
</a-form-item>
<a-form-item label="分类名称" name="title">
<a-input
allow-clear
placeholder="请输入分类名称"
v-model:value="form.title"
@pressEnter="save"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="排序号" name="sortNumber">
<a-input-number
:min="0"
:max="99999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="是否展示">
<a-switch
checked-children="是"
un-checked-children="否"
:checked="form.status === 0"
@update:checked="updateHideValue"
/>
</a-form-item>
</a-col>
</a-row>
<div style="margin-bottom: 22px">
<a-divider />
</div>
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="索引图标" name="image">
<ele-image-upload
v-model:value="images"
:item-style="{ width: '45px', height: '45px' }"
:limit="1"
@upload="onUpload"
@remove="onClose"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item
label="备注"
name="comments"
:label-col="
styleResponsive ? { md: 3, sm: 4, xs: 24 } : { flex: '90px' }
"
:wrapper-col="
styleResponsive ? { md: 21, sm: 20, xs: 24 } : { flex: '1' }
"
>
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注信息"
v-model:value="form.comments"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import type { Category } from '@/api/goods/category/model';
import { addCategory, updateCategory } from '@/api/goods/category';
import { FILE_SERVER } from "@/config/setting";
import { ItemType } from "ele-admin-pro/es/ele-image-upload/types";
import { uploadFile } from "@/api/system/file";
import { LoginRecordParam } from "@/api/system/login-record/model";
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Category | null;
// 上级分类id
parentId?: number;
// 全部分类数据
categoryList: Category[];
}>();
// 表单数据
const { form, resetFields, assignFields } = useFormData<Category>({
title: '',
categoryId: undefined,
parentId: undefined,
image: '',
status: 0,
sortNumber: 100
});
//
const formRef = ref<FormInstance | null>(null);
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 已上传数据, 可赋初始值用于回显
const images = ref([]);
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
title: [
{
required: true,
message: '请输入分类名称',
type: 'string',
trigger: 'blur'
}
],
sortNumber: [
{
required: true,
message: '请输入排序号',
type: 'number',
trigger: 'blur'
}
],
meta: [
{
type: 'string',
validator: async (_rule: Rule, value: string) => {
if (value) {
const msg = '请输入正确的JSON格式';
try {
const obj = JSON.parse(value);
if (typeof obj !== 'object' || obj === null) {
return Promise.reject(msg);
}
} catch (_e) {
return Promise.reject(msg);
}
}
return Promise.resolve();
},
trigger: 'blur'
}
]
});
// 上传文件
const onUpload = (d: ItemType) => {
uploadFile(<File>d.file)
.then((result) => {
form.image = result.path;
message.success('上传成功');
})
.catch((e) => {
message.error(e.message);
});
};
const onClose = () => {
form.image = ''
}
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const categoryForm = {
...form,
parentId: form.parentId || 0
};
const saveOrUpdate = isUpdate.value ? updateCategory : addCategory;
saveOrUpdate(categoryForm)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const updateHideValue = (value: boolean) => {
form.status = value ? 0 : 1;
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
// 头像赋值
images.value = [];
if(props.data.image){
images.value.push({ uid:1, url: props.data.image, status: '' });
}
assignFields({
...props.data,
parentId:
props.data.parentId === 0 ? undefined : props.data.parentId
});
isUpdate.value = true;
} else {
form.parentId = props.parentId;
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>
<script lang="ts">
import * as icons from '@/layout/menu-icons';
export default {
components: icons,
data() {
return {
iconData: [
{
title: '已引入的图标',
icons: Object.keys(icons)
}
]
};
}
};
</script>
@@ -0,0 +1,281 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="categoryId"
:columns="columns"
:datasource="datasource"
:parse-data="parseData"
:need-page="false"
:expand-icon-column-index="1"
:expanded-row-keys="expandedRowKeys"
:scroll="{ x: 1200 }"
cache-key="proGoodsCategoryTable"
@done="onDone"
@expand="onExpand"
>
<template #toolbar>
<a-space>
<a-button type="primary" class="ele-btn-icon" @click="openEdit()">
<template #icon>
<plus-outlined />
</template>
<span>新建</span>
</a-button>
<a-button type="dashed" class="ele-btn-icon" @click="expandAll">
展开全部
</a-button>
<a-button type="dashed" class="ele-btn-icon" @click="foldAll">
折叠全部
</a-button>
<!-- 搜索表单 -->
<a-input-search
allow-clear
v-model:value="searchText"
placeholder="请输入搜索关键词"
@search="search"
@pressEnter="search"
/>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'menuType'">
<a-tag v-if="isExternalLink(record.path)" color="red">外链</a-tag>
<a-tag v-else-if="isExternalLink(record.component)" color="orange">
内链
</a-tag>
<a-tag v-else-if="isDirectory(record)" color="blue">目录</a-tag>
<a-tag v-else-if="record.menuType === 0" color="green">分类</a-tag>
<a-tag v-else-if="record.menuType === 1">按钮</a-tag>
</template>
<template v-else-if="column.key === 'title'">
<a-avatar
:size="20"
v-if="record.image"
shape="square"
:src="`${record.image}`"
/>
<a-tooltip :title="`分类ID:${record.categoryId}`">
<span>{{ record.title }}</span>
</a-tooltip>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="orange">隐藏</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a @click="openEdit(null, record.categoryId)">添加</a>
<a-divider type="vertical" />
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
placement="topRight"
title="确定要删除此分类吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<category-edit
v-model:visible="showEdit"
:data="current"
:parent-id="parentId"
:category-list="categoryData"
@done="reload"
/>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue/es';
import { PlusOutlined } from '@ant-design/icons-vue';
import type {
DatasourceFunction,
ColumnItem,
EleProTableDone
} from 'ele-admin-pro/es/ele-pro-table/types';
import {
messageLoading,
toDateString,
isExternalLink,
toTreeData,
eachTreeData
} from 'ele-admin-pro/es';
import type { EleProTable } from 'ele-admin-pro/es';
import CategoryEdit from './components/category-edit.vue';
import type { Category, CategoryParam } from '@/api/goods/category/model';
import { listCategory, removeCategory } from '@/api/goods/category';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '分类名称',
key: 'title',
showSorterTooltip: false,
ellipsis: true
},
{
title: '排序',
dataIndex: 'sortNumber',
sorter: true,
width: 180,
showSorterTooltip: false
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
sorter: true,
width: 180,
showSorterTooltip: false,
customRender: ({ text }) => ['显示', '隐藏'][text]
},
{
title: '创建时间',
dataIndex: 'createTime',
sorter: true,
showSorterTooltip: false,
ellipsis: true,
width: 180,
customRender: ({ text }) => toDateString(text)
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center'
}
]);
// 当前编辑数据
const current = ref<Category | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 上级分类id
const parentId = ref<number>();
// 分类数据
const categoryData = ref<Category[]>([]);
// 表格展开的行
const expandedRowKeys = ref<number[]>([]);
// 表格数据源
const datasource: DatasourceFunction = ({ where }) => {
return listCategory({ ...where });
};
/* 数据转为树形结构 */
const parseData = (data: Category[]) => {
return toTreeData({
data: data.map((d) => {
return { ...d, key: d.categoryId, value: d.categoryId };
}),
idField: 'categoryId',
parentIdField: 'parentId'
});
};
/* 表格渲染完成回调 */
const onDone: EleProTableDone<Category> = ({ data }) => {
categoryData.value = data;
};
/* 刷新表格 */
const reload = (where?: CategoryParam) => {
tableRef?.value?.reload({ where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: Category | null, id?: number) => {
current.value = row ?? null;
parentId.value = id;
showEdit.value = true;
};
const search = (searchText) => {
reload({ title: searchText });
};
/* 删除单个 */
const remove = (row: Category) => {
if (row.children?.length) {
message.error('请先删除子节点');
return;
}
const hide = messageLoading('请求中..', 0);
removeCategory(row.categoryId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 展开全部 */
const expandAll = () => {
let keys: number[] = [];
eachTreeData(categoryData.value, (d) => {
if (d.children && d.children.length && d.categoryId) {
keys.push(d.categoryId);
}
});
expandedRowKeys.value = keys;
};
/* 折叠全部 */
const foldAll = () => {
expandedRowKeys.value = [];
};
/* 点击展开图标时触发 */
const onExpand = (expanded: boolean, record: Category) => {
if (expanded) {
expandedRowKeys.value = [
...expandedRowKeys.value,
record.categoryId as number
];
} else {
expandedRowKeys.value = expandedRowKeys.value.filter(
(d) => d !== record.categoryId
);
}
};
/* 判断是否是目录 */
const isDirectory = (d: Category) => {
return !!d.children?.length;
};
</script>
<script lang="ts">
export default {
name: 'Category'
};
</script>
@@ -0,0 +1,727 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
width="80%"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑设备' : '添加设备'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<div style="background-color: #f3f3f3; padding: 8px">
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ md: { span: 7 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 17 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<a-card title="基本信息" :bordered="false">
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="设备名称" name="name">
<DictSelect
dict-code="DeviceAssignment"
placeholder="请选择设备"
v-model:value="form.name"
/>
</a-form-item>
<a-form-item label="出厂编号" name="factoryNo">
<a-input
allow-clear
placeholder="请输入出厂编号(不能重复)"
v-model:value="form.factoryNo"
/>
</a-form-item>
<a-form-item label="自编号" name="equipmentNo">
<a-input
allow-clear
placeholder="请输入设备编号"
v-model:value="form.equipmentNo"
/>
</a-form-item>
<a-form-item label="剩余使用年限" name="surplusYear">
<a-input
allow-clear
placeholder="请输入插件名称"
disabled
v-model:value="form.surplusYear"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="设备型号" name="model">
<DictSelect
dict-code="EquipmentModel"
placeholder="请选择设备型号"
v-model:value="form.model"
/>
</a-form-item>
<a-form-item label="出厂日期" name="factoryDate">
<a-date-picker
class="ele-fluid"
value-format="YYYY-MM-DD"
placeholder="请选择日期"
v-model:value="form.factoryDate"
/>
</a-form-item>
<a-form-item label="使用年限" name="lifeYear">
<a-input
allow-clear
placeholder="请输入使用年限"
v-model:value="form.lifeYear"
@change="onLifeYaer"
/>
</a-form-item>
<a-form-item label="产权单位" name="company">
<TowerCompany
:placeholder="`请选择租赁单位`"
v-model:value="form.company"
:customer-type="`产权单位`"
@done="chooseCompanyName"
/>
<!-- <DictSelect-->
<!-- dict-code="propertyCompany"-->
<!-- placeholder="请选择产权单位"-->
<!-- v-model:value="form.company"-->
<!-- />-->
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="备案编号" name="filingNo">
<a-input
allow-clear
placeholder="请输入备案编号"
v-model:value="form.filingNo"
/>
</a-form-item>
<a-form-item label="制造厂家" name="manufactor">
<DictSelect
dict-code="Manufacturer"
placeholder="请选择制造厂家"
v-model:value="form.manufactor"
/>
</a-form-item>
<a-form-item label="报废日期" name="scrapDate">
<a-date-picker
class="ele-fluid"
value-format="YYYY-MM-DD"
placeholder="请选择日期"
v-model:value="form.scrapDate"
/>
</a-form-item>
<a-form-item label="备案许可证号" name="licenceNo">
<a-input
allow-clear
placeholder="请输入插件名称"
v-model:value="form.licenceNo"
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
<a-divider style="height: 8px" />
<a-card title="设备信息" :bordered="false">
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="所属仓库" name="warehouse">
<SelectWarehouse
:placeholder="`请选择仓库`"
v-model:value="form.warehouse"
@done="chooseWarehouse"
/>
</a-form-item>
<a-form-item label="采购日期" name="title">
<a-date-picker
class="ele-fluid"
value-format="YYYY-MM-DD"
placeholder="请选择日期"
v-model:value="form.buyDate"
/>
</a-form-item>
<a-form-item label="当前位置" name="currentLocation">
<a-input
allow-clear
disabled
placeholder="请输入当前位置"
v-model:value="form.currentLocation"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="所属人员" name="users">
<SelectUser
:placeholder="`请选择所属人员`"
v-model:value="form.users"
@done="chooseUsers"
/>
</a-form-item>
<a-form-item label="安全管理部门" name="organization">
<SelectOrganization
:placeholder="`请选择所属部门`"
v-model:value="form.organization"
@done="chooseOrganization"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="设备来源" name="source">
<a-input
allow-clear
placeholder="请输入设备来源"
v-model:value="form.source"
/>
</a-form-item>
<a-form-item label="机号" name="machineNo">
<a-input
allow-clear
placeholder="请输入插件名称"
v-model:value="form.machineNo"
/>
</a-form-item>
<a-form-item label="是否外协标识" name="isOutOrg">
<a-switch v-model:checked="form.isOutOrg" />
</a-form-item>
</a-col>
</a-row>
</a-card>
<a-divider style="height: 8px" />
<a-card title="设备参数" :bordered="false">
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="设计高度" name="designHeight">
<a-input-number
allow-clear
:min="0"
style="width: 200px"
placeholder="请输入设计高度"
v-model:value="form.designHeight"
/>
<span class="ml-10">m</span>
</a-form-item>
<a-form-item label="独立高度" name="height">
<a-input-number
allow-clear
:min="0"
style="width: 200px"
placeholder="请输入独立高度"
v-model:value="form.height"
/>
<span class="ml-10">m</span>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 16 }
"
>
<a-form-item label="额定载重" name="ratedLoad">
<a-input-number
allow-clear
:min="0"
style="width: 200px"
placeholder="请输入额定载重"
v-model:value="form.ratedLoad"
/>
<span class="ml-10">T</span>
</a-form-item>
<a-form-item label="最大幅度处额定载重" name="maxRatedLoad">
<a-input-number
allow-clear
:min="0"
style="width: 200px"
placeholder="请输入最大幅度处额定载重"
v-model:value="form.maxRatedLoad"
/>
<span class="ml-10">T</span>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="最大起升高度(m)" name="maxLiftingHeight">
<a-input-number
allow-clear
:min="0"
style="width: 200px"
placeholder="请输入最大起升高度(m)"
v-model:value="form.maxLiftingHeight"
/>
<span class="ml-10">m</span>
</a-form-item>
<a-form-item label="臂长" name="armLength">
<a-input-number
allow-clear
:min="0"
style="width: 200px"
placeholder="请输入臂长"
v-model:value="form.armLength"
/>
<span class="ml-10">m</span>
</a-form-item>
</a-col>
</a-row>
</a-card>
<a-divider style="height: 8px" />
<a-card title="附件信息" :bordered="false">
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="采购合同" name="parentId">
<a-upload
v-model:file-list="file1"
name="file"
action="https://file.wsdns.cn/api/file/image"
:headers="{ Authorization: token }"
@change="onFile1"
>
<a-button>
<upload-outlined />
上传
</a-button>
</a-upload>
</a-form-item>
<a-form-item label="发票" name="parentId">
<a-upload
v-model:file-list="file2"
name="file"
action="https://file.wsdns.cn/api/file/image"
:headers="{ Authorization: token }"
@change="onFile2"
>
<a-button>
<upload-outlined />
上传
</a-button>
</a-upload>
</a-form-item>
<a-form-item label="制造许可证" name="parentId">
<a-upload
v-model:file-list="file3"
name="file"
action="https://file.wsdns.cn/api/file/image"
:headers="{ Authorization: token }"
@change="onFile3"
>
<a-button>
<upload-outlined />
上传
</a-button>
</a-upload>
</a-form-item>
<a-form-item label="出厂合格证" name="parentId">
<a-upload
v-model:file-list="file4"
name="file"
action="https://file.wsdns.cn/api/file/image"
:headers="{ Authorization: token }"
@change="onFile4"
>
<a-button>
<upload-outlined />
上传
</a-button>
</a-upload>
</a-form-item>
<a-form-item label="备案证书" name="parentId">
<a-upload
v-model:file-list="file5"
name="file"
action="https://file.wsdns.cn/api/file/image"
:headers="{ Authorization: token }"
@change="onFile5"
>
<a-button>
<upload-outlined />
上传
</a-button>
</a-upload>
</a-form-item>
<a-form-item label="整机性能参数表" name="parentId">
<a-upload
v-model:file-list="file6"
name="file"
action="https://file.wsdns.cn/api/file/image"
:headers="{ Authorization: token }"
@change="onFile6"
>
<a-button>
<upload-outlined />
上传
</a-button>
</a-upload>
</a-form-item>
<a-form-item label="其他" name="parentId">
<a-upload
v-model:file-list="file7"
name="file"
action="https://file.wsdns.cn/api/file/image"
:headers="{ Authorization: token }"
@change="onFile7"
>
<a-button>
<upload-outlined />
上传
</a-button>
</a-upload>
</a-form-item>
</a-col>
</a-row>
</a-card>
</a-form>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import {
addTowerEquipment,
updateTowerEquipment
} from '@/api/tower/equipment';
import { TowerEquipment } from '@/api/tower/equipment/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { UploadOutlined } from '@ant-design/icons-vue';
import DictSelect from '@/views/search/components/dict-select.vue';
import { FormInstance } from 'ant-design-vue/es/form';
import { TOKEN_STORE_NAME } from '@/config/setting';
import { Warehouse } from '@/api/tower/warehouse/model';
import { User } from '@/api/user/model';
import { Organization } from '@/api/system/organization/model';
import { Customer } from '@/api/oa/customer/model';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: TowerEquipment | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
const formRef = ref<FormInstance | null>(null);
interface FileItem {
uid: string;
name?: string;
status?: string;
response?: Response;
thumbUrl?: string;
downloadUrl?: string;
url: string;
}
interface FileInfo {
file: FileItem;
fileList: FileItem[];
}
// 文件上传
const file1 = ref<FileItem[]>();
const file2 = ref<FileItem[]>();
const file3 = ref<FileItem[]>();
const file4 = ref<FileItem[]>();
const file5 = ref<FileItem[]>();
const file6 = ref<FileItem[]>();
const file7 = ref<FileItem[]>();
// token
const token = localStorage.getItem(TOKEN_STORE_NAME);
// 表单信息
const form = reactive<TowerEquipment>({
equipmentId: undefined,
name: undefined,
model: undefined,
equipmentNo: '',
factoryNo: '',
factoryDate: '',
manufactor: undefined,
company: '',
lifeYear: undefined,
surplusYear: 3,
scrapDate: '',
licenceNo: '',
organization: undefined,
warehouse: undefined,
source: undefined,
buyDate: '',
currentLocation: '',
designHeight: undefined,
height: undefined,
ratedLoad: '',
files: '[]',
file1: '[]',
file2: '[]',
file3: '[]',
file4: '[]',
file5: '[]',
file6: '[]',
file7: '[]',
filingNo: '',
machineNo: '',
users: '',
isOutOrg: false,
status: undefined,
comments: '',
userId: undefined,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
maxRatedLoad: '',
maxLiftingHeight: '',
armLength: ''
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
name: [
{
required: true,
type: 'string',
message: '请输入设备名称',
trigger: 'blur'
}
],
model: [
{
required: true,
type: 'string',
message: '请输入设备型号',
trigger: 'blur'
}
],
factoryNo: [
{
required: true,
type: 'string',
message: '请输入出厂编号',
trigger: 'blur'
}
]
});
const { resetFields } = useForm(form, rules);
const onLifeYaer = () => {
form.surplusYear = form.lifeYear;
};
// const onFactoryDate = () => {
// form.scrapDate = form.factoryDate;
// };
const onFile1 = (info: FileInfo) => {
let resFileList = [...info.fileList];
file1.value = resFileList.map((file) => {
if (file.response) {
// Component will show file.url as link
file.url = file.response.url;
}
return file;
});
form.file1 = JSON.stringify(file1.value);
};
const onFile2 = (info: FileInfo) => {
let resFileList = [...info.fileList];
file2.value = resFileList.map((file) => {
if (file.response) {
// Component will show file.url as link
file.url = file.response.url;
}
return file;
});
form.file2 = JSON.stringify(file2.value);
};
const onFile3 = (info: FileInfo) => {
let resFileList = [...info.fileList];
file3.value = resFileList.map((file) => {
if (file.response) {
// Component will show file.url as link
file.url = file.response.url;
}
return file;
});
form.file3 = JSON.stringify(file3.value);
};
const onFile4 = (info: FileInfo) => {
let resFileList = [...info.fileList];
file4.value = resFileList.map((file) => {
if (file.response) {
// Component will show file.url as link
file.url = file.response.url;
}
return file;
});
form.file4 = JSON.stringify(file4.value);
};
const onFile5 = (info: FileInfo) => {
let resFileList = [...info.fileList];
file5.value = resFileList.map((file) => {
if (file.response) {
// Component will show file.url as link
file.url = file.response.url;
}
return file;
});
form.file5 = JSON.stringify(file5.value);
};
const onFile6 = (info: FileInfo) => {
let resFileList = [...info.fileList];
file6.value = resFileList.map((file) => {
if (file.response) {
// Component will show file.url as link
file.url = file.response.url;
}
return file;
});
form.file6 = JSON.stringify(file6.value);
};
const onFile7 = (info: FileInfo) => {
let resFileList = [...info.fileList];
file7.value = resFileList.map((file) => {
if (file.response) {
// Component will show file.url as link
file.url = file.response.url;
}
return file;
});
form.file7 = JSON.stringify(file7.value);
};
const chooseWarehouse = (data: Warehouse) => {
console.log(data);
form.warehouse = data.warehouseName;
form.currentLocation = data.address;
};
const chooseUsers = (data: User) => {
form.users = data.nickname;
};
const chooseOrganization = (data: Organization) => {
form.organization = data.organizationName;
};
const chooseCompanyName = (data: Customer) => {
form.company = data.customerName;
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value
? updateTowerEquipment
: addTowerEquipment;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignObject(form, props.data);
if (props.data.file1) {
file1.value = JSON.parse(props.data.file1);
}
if (props.data.file2) {
file2.value = JSON.parse(props.data.file2);
}
if (props.data.file3) {
file3.value = JSON.parse(props.data.file3);
}
if (props.data.file4) {
file4.value = JSON.parse(props.data.file4);
}
if (props.data.file5) {
file5.value = JSON.parse(props.data.file5);
}
if (props.data.file6) {
file6.value = JSON.parse(props.data.file6);
}
if (props.data.file7) {
file7.value = JSON.parse(props.data.file7);
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>
<style lang="less"></style>
<style lang="less">
.tab-pane {
min-height: 300px;
}
.ml-10 {
margin-left: 5px;
}
.upload-text {
margin-right: 70px;
}
</style>
@@ -0,0 +1,111 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
:width="600"
:visible="visible"
:maskClosable="false"
:title="`导入设备`"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
:footer="null"
>
<a-upload-dragger
name="file"
:show-upload-list="false"
accept=".xls,.xlsx"
:before-upload="handleImport"
>
<p class="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p class="ant-upload-text">单击或拖动文件到此区域进行上传</p>
<p class="ant-upload-hint" style="max-width: 550px">
提示:附件上传中,请不要做任何操作! 支持的文件类型为*.xls, *.xlsx!
</p>
</a-upload-dragger>
<div style="margin: 20px auto; display: flex; justify-content: center">
<a
href="https://file.wsdns.cn/download/20230523/2f5ffa06b0684e138184c6786f6fbb68.xlsx"
target="_blank"
>下载设备导入模板</a
>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { InboxOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import { addBatchTowerEquipment } from '@/api/tower/equipment';
import { TowerEquipment } from '@/api/tower/equipment/model';
import { read, utils } from 'xlsx';
import { ref } from 'vue';
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 导入的数据
const importData = ref<TowerEquipment[]>([]);
const handleImport = (file: File) => {
if (
![
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
].includes(file.type)
) {
message.error('只能选择 excel 文件');
return false;
}
if (file.size / 1024 / 1024 > 20) {
message.error('大小不能超过 20MB');
return false;
}
const reader = new FileReader();
reader.onload = (e) => {
const data = new Uint8Array(e.target?.result as any);
const workbook = read(data, { type: 'array' });
const sheetNames = workbook.SheetNames;
const worksheet = workbook.Sheets[sheetNames[0]];
// 解析成二维数组
const aoa = utils.sheet_to_json<string[]>(worksheet, { header: 1 });
console.log(aoa);
aoa.slice(1).map((d) => {
const array = ['闲置', '报废', '注销', '转卖'];
importData.value.push({
status: array.indexOf(d[0]),
name: d[1],
factoryNo: d[2],
model: d[3],
manufactor: d[4],
buyDate: d[5],
filingNo: d[6],
factoryDate: d[7],
company: d[8],
source: d[9],
warehouse: d[10],
users: d[11],
equipmentNo: d[12],
ratedLoad: d[13],
maxLiftingHeight: d[14],
armLength: d[15],
currentLocation: d[16],
machineNo: d[17]
});
});
addBatchTowerEquipment(importData.value).then(() => {
message.success('导入成功');
updateVisible(false);
emit('done');
});
};
reader.readAsArrayBuffer(file);
return false;
};
</script>
@@ -0,0 +1,376 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
:width="`80%`"
:visible="visible"
:confirm-loading="loading"
:maxable="maxAble"
:title="isUpdate ? '编辑订单' : '订单详情'"
:body-style="{ paddingBottom: '8px', background: '#f3f3f3' }"
@update:visible="updateVisible"
:maskClosable="false"
:footer="null"
@ok="save"
>
<!-- <a-card class="order-card" :bordered="false">-->
<!-- <a-steps-->
<!-- :current="active"-->
<!-- direction="horizontal"-->
<!-- :responsive="styleResponsive"-->
<!-- >-->
<!-- <template v-for="(item, index) in steps" :key="index">-->
<!-- <a-step-->
<!-- :title="item.title"-->
<!-- :description="timeAgo(item.description)"-->
<!-- />-->
<!-- </template>-->
<!-- </a-steps>-->
<!-- </a-card>-->
<a-card title="订单详情" class="order-card">
<!-- <a-space>-->
<!-- <a-button>发货</a-button>-->
<!-- <a-button>商家备注</a-button>-->
<!-- <a-button>打印小票</a-button>-->
<!-- </a-space>-->
<a-row :gutter="16">
<a-col
v-bind="
styleResponsive
? { xl: 8, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 8 }
"
>
<a-form-item label="订单号" name="orderId">
<span>{{ data.orderId }}</span>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 8, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 8 }
"
>
<a-form-item label="实付款金额" name="payPrice">
<span class="ele-text-warning"
>¥{{ formatNumber(data.payPrice) }}</span
>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 8, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 8 }
"
>
<a-form-item label="订单状态" name="orderStatus">
<a-tag>{{ data.payStatus === 20 ? '已下单' : '' }}</a-tag>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 8, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 8 }
"
>
<a-form-item label="买家信息" name="deliveryType">
<router-link :to="'/system/user/details?id=' + data.userId">
<span class="ele-text-primary">{{ data.nickname }}</span>
</router-link>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 8, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 8 }
"
>
<a-form-item label="预定日期" name="deliveryTime">
{{ toDateString(data.deliveryTime, 'yyyy-MM-dd') }}
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 8, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 8 }
"
>
<a-form-item label="下单时间" name="createTime">
{{ data.createTime }}
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 8, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 8 }
"
>
<a-form-item label="交易流水号" name="orderNo">
{{ data.orderNo }}
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 8, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 8 }
"
>
<a-form-item label="买家留言" name="buyerRemark">
<span>{{ data.buyerRemark }}</span>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 8, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 8 }
"
>
<a-form-item label="订单备注" name="comments">
<span>{{ data.comments }}</span>
</a-form-item>
</a-col>
</a-row>
</a-card>
<a-card title="菜品信息" class="order-card">
<a-spin :spinning="loading">
<a-table
:data-source="orderGoodsList"
:columns="columns"
:pagination="false"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'goodsName'">
<div class="goods-info">
<a-image
v-if="record.imageUrl"
:src="record.imageUrl"
:preview="false"
:width="50"
/>
<div class="info">
<div>{{ record.goodsName }}</div>
<div class="ele-text-placeholder">{{ record.comments }}</div>
</div>
</div>
</template>
</template>
</a-table>
</a-spin>
</a-card>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form } from 'ant-design-vue';
import { assignObject, toDateString } from 'ele-admin-pro';
import { useThemeStore } from '@/store/modules/theme';
import { formatNumber } from 'ele-admin-pro/es';
import { storeToRefs } from 'pinia';
import { Order } from '@/api/order/model';
import { ColumnItem } from 'ele-admin-pro/es/ele-pro-table/types';
import { OrderGoods } from '@/api/order/goods/model';
import { listOrderGoods } from '@/api/order/goods';
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Order | null;
}>();
export interface step {
title?: String | undefined;
subTitle?: String | undefined;
description?: String | undefined;
}
// 是否是修改
const isUpdate = ref(false);
// 是否显示最大化切换按钮
const maxAble = ref(true);
const orderGoodsList = ref<OrderGoods[]>([]);
// 步骤条
const steps = ref<step[]>([
{
title: '报餐',
description: undefined
},
{
title: '付款',
description: undefined
},
{
title: '发餐',
description: undefined
},
{
title: '取餐',
description: undefined
},
{
title: '完成',
description: undefined
}
]);
const active = ref(2);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 订单信息
const order = reactive<Order>({
orderId: undefined,
orderNo: '',
userId: undefined,
orderSourceData: ''
});
// 请求状态
const loading = ref(true);
const { resetFields } = useForm(order);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const columns = ref<ColumnItem[]>([
// {
// title: '菜品ID',
// dataIndex: 'goodsId'
// },
{
title: '菜品信息',
dataIndex: 'goodsName',
key: 'goodsName'
},
{
title: '菜品价格',
dataIndex: 'goodsPrice',
customRender: ({ text }) => '¥' + text
},
{
title: '购买数量',
dataIndex: 'totalNum',
key: 'totalNum'
}
]);
/* 制作步骤条 */
const loadSteps = (order) => {
steps.value = [];
steps.value.push({
title: '下单'
});
steps.value.push({
title: '付款'
});
steps.value.push({
title: '发货'
});
steps.value.push({
title: '收货'
});
steps.value.push({
title: '完成'
});
// 下单
if (order.payStatus == 10) {
active.value = 0;
steps.value[0].description = order.createTime;
}
// 付款
if (order.payStatus == 20) {
active.value = 1;
steps.value[0].description = order.createTime;
steps.value[1].description = order.payTime;
}
// 发货
if (order.payStatus == 20 && order.deliveryStatus == 20) {
active.value = 2;
steps.value[0].description = order.createTime;
steps.value[1].description = order.payTime;
steps.value[2].description = order.deliveryTime;
}
// 收货
if (order.payStatus == 20 && order.receiptStatus == 20) {
active.value = 3;
steps.value[0].description = order.createTime;
steps.value[1].description = order.payTime;
steps.value[2].description = order.deliveryTime;
steps.value[3].description = order.receiptTime;
}
// 完成
if (order.payStatus == 20 && order.orderStatus == 30) {
active.value = 4;
steps.value[0].description = order.createTime;
steps.value[1].description = order.payTime;
steps.value[2].description = order.deliveryTime;
steps.value[3].description = order.receiptTime;
}
// 已取消
if (order.orderStatus == 20) {
active.value = 4;
}
};
const getOrderGoods = () => {
const orderId = props.data?.orderId;
listOrderGoods({ orderId }).then((data) => {
orderGoodsList.value = data;
});
};
/* 保存编辑 */
const save = () => {};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
loading.value = false;
assignObject(order, props.data);
loadSteps(props.data);
getOrderGoods();
}
} else {
resetFields();
}
}
);
</script>
<style lang="less" scoped>
.order-card {
margin-bottom: 20px;
}
.ant-form-item {
margin-bottom: 5px;
}
.goods-info {
display: flex;
.info {
padding-left: 5px;
display: flex;
flex-direction: column;
}
}
</style>
@@ -0,0 +1,203 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加设备</span>
</a-button>
<a-button
danger
type="primary"
class="ele-btn-icon"
:disabled="selection.length === 0"
@click="removeBatch"
>
<template #icon>
<DeleteOutlined />
</template>
<span>批量删除</span>
</a-button>
<a-button class="ele-btn-icon" @click="openImport">
<template #icon>
<UploadOutlined />
</template>
<span>导入设备</span>
</a-button>
<a-button class="ele-btn-icon" @click="handleExport">
<template #icon>
<DownloadOutlined />
</template>
<span>导出设备</span>
</a-button>
<a-radio-group v-model:value="status" @change="handleStatus">
<a-radio-button :value="1">报废</a-radio-button>
<a-radio-button :value="2">注销</a-radio-button>
<a-radio-button :value="0">恢复</a-radio-button>
<a-radio-button :value="3">转卖</a-radio-button>
</a-radio-group>
<a-input-search
allow-clear
placeholder="请输入关键词"
v-model:value="searchText"
@pressEnter="search"
@search="search"
/>
<!-- 导入弹窗 -->
<Import v-model:visible="showImport" @done="search" />
</a-space>
</template>
<script lang="ts" setup>
import {
PlusOutlined,
UploadOutlined,
DownloadOutlined,
DeleteOutlined
} from '@ant-design/icons-vue';
import useSearch from '@/utils/use-search';
import { ref, watch } from 'vue';
import { utils, writeFile } from 'xlsx';
import { TowerEquipment } from '@/api/tower/equipment/model';
import { TowerEquipmentParam } from '@/api/tower/equipment/model';
import Import from './import.vue';
import { message } from 'ant-design-vue';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: TowerEquipmentParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'update', status?: number): void;
(e: 'import'): void;
}>();
// 表单数据
const { where, resetFields } = useSearch<TowerEquipmentParam>({
equipmentId: undefined,
name: undefined,
keywords: ''
});
// 下来选项
const type = ref('keywords');
// 搜索内容
const searchText = ref('');
// 状态
const status = ref<number>();
// 是否显示导入弹窗
const showImport = ref(false);
/* 搜索 */
const search = () => {
resetFields();
if (type.value == 'keywords') {
where.keywords = searchText.value;
}
emit('search', {
...where
});
};
// 导出
const handleExport = () => {
if (!props.selection?.length) {
message.error('请至少选择一条数据');
return;
}
const array: (string | number)[][] = [
[
'设备状态',
'设备名称',
'出厂编号',
'设备型号',
'制造厂商',
'采购日期',
'备案编号',
'出厂日期',
'产权单位',
'设备来源',
'所属仓库',
'所属人员',
'自编号',
'额定载重',
'最大起升高度(m)',
'臂长',
'当前位置',
'楼号'
]
];
props.selection?.forEach((d: TowerEquipment) => {
array.push([
`${['闲置', '报废', '注销', '转卖'][Number(d.status)]}`,
`${d.name}`,
`${d.factoryNo}`,
`${d.model}`,
`${d.manufactor}`,
`${d.buyDate ? d.buyDate : ''}`,
`${d.filingNo}`,
`${d.factoryDate ? d.factoryDate : ''}`,
`${d.company}`,
`${d.source}`,
`${d.warehouse}`,
`${d.users}`,
`${d.equipmentNo}`,
`${d.ratedLoad}`,
`${d.maxLiftingHeight}`,
`${d.armLength}`,
`${d.currentLocation}`,
`${d.machineNo}`
]);
});
const sheetName = '设备导出列表';
const workbook = {
SheetNames: [sheetName],
Sheets: {}
};
const sheet = utils.aoa_to_sheet(array);
workbook.Sheets[sheetName] = sheet;
// 设置列宽
sheet['!cols'] = [
{ wch: 10 },
{ wch: 10 },
{ wch: 10 },
{ wch: 10 },
{ wch: 20 },
{ wch: 40 },
{ wch: 10 }
];
writeFile(workbook, '导出设备管理表.xlsx');
};
// 发布设备
const add = () => {
emit('add');
};
// 导入
const openImport = () => {
showImport.value = true;
};
const handleStatus = () => {
emit('update', status.value);
};
// 批量删除
const removeBatch = () => {
emit('remove');
};
watch(
() => props.selection,
() => {}
);
</script>
+282
View File
@@ -0,0 +1,282 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="equipmentId"
:columns="columns"
:datasource="datasource"
v-model:selection="selection"
:customRow="customRow"
tool-class="ele-toolbar-form"
:scroll="{ x: 1200 }"
class="sys-org-table"
:striped="true"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@update="handleStatus"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'nickname'">
{{ record.nickname }}
</template>
<template v-if="column.key === 'createTime'">
{{ record.createTime }}
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">闲置</a-tag>
<a-tag v-if="record.status === 1" color="red">报废</a-tag>
<a-tag v-if="record.status === 2" color="purple">注销</a-tag>
<a-tag v-if="record.status === 3" color="orange">转卖</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">一机一档</a>
<a-divider type="vertical" />
<a @click="openEdit(record)">设备名片</a>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
</div>
<!-- 编辑弹窗 -->
<EquipmentEdit v-model:visible="showEdit" :data="current" @done="reload" />
</div>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { toDateString } from 'ele-admin-pro';
import Search from './components/search.vue';
import EquipmentEdit from './components/equipment-edit.vue';
import {
pageTowerEquipment,
removeBatchTowerEquipment,
updateBatchTowerEquipment
} from '@/api/tower/equipment';
import {
TowerEquipment,
TowerEquipmentParam
} from '@/api/tower/equipment/model';
defineProps<{
activeKey?: boolean;
data?: any;
}>();
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '设备状态',
dataIndex: 'status',
key: 'status',
customRender: ({ text }) => ['闲置', '报废', '注销', '转卖'][text]
},
{
title: '设备名称',
dataIndex: 'name',
key: 'name'
},
{
title: '设备型号',
dataIndex: 'model',
key: 'model'
},
{
title: '制造厂商',
dataIndex: 'manufactor',
key: 'manufactor'
},
{
title: '当前位置',
dataIndex: 'currentLocation',
key: 'currentLocation'
},
{
title: '产权单位(个人)',
dataIndex: 'company',
key: 'company'
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
customRender: ({ text }) => toDateString(text)
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center',
hideInSetting: true
}
]);
// 表格选中数据
const selection = ref<TowerEquipment[]>([]);
// 当前编辑数据
const current = ref<TowerEquipment | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
// 搜索条件
if (filters.payMethod) {
where.payMethod = filters.payMethod;
}
if (filters.deliveryType) {
where.deliveryType = filters.deliveryType;
}
if (filters.payStatus) {
where.payStatus = filters.payStatus;
}
if (filters.orderSource) {
where.orderSource = filters.orderSource;
}
where.tenantId = localStorage.getItem('tenantId');
where.payStatus = 20;
return pageTowerEquipment({
...where,
...orders,
page,
limit
});
};
/* 搜索 */
const reload = (where?: TowerEquipmentParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: TowerEquipment) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 批量修改 */
const handleStatus = (status) => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
const data = selection.value.map((d) => {
return {
equipmentId: d.equipmentId,
status
};
});
console.log(data);
updateBatchTowerEquipment(data).then((msg) => {
message.success(msg);
reload();
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchTowerEquipment(selection.value.map((d) => d.equipmentId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 自定义行属性 */
const customRow = (record: TowerEquipment) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
reload();
</script>
<script lang="ts">
export default {
name: 'TowerEquipmentIndex'
};
</script>
<style lang="less" scoped>
p {
line-height: 0.8;
}
.sys-org-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.sys-org-table :deep(.ant-table-pagination.ant-pagination) {
padding: 0 4px;
margin-bottom: 0;
}
.price-edit {
padding-right: 5px;
}
.comments {
max-width: 200px;
}
</style>
@@ -0,0 +1,266 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
:width="750"
:visible="visible"
:confirm-loading="loading"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑派单' : '添加派单'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
:label-col="{ md: { span: 6 }, sm: { span: 20 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 24 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<a-row :gutter="16">
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="派单名称" v-bind="validateInfos.customerName">
<a-input
allow-clear
:maxlength="30"
placeholder="请输入派单名称"
v-model:value="form.customerName"
@blur="
validate('customerName', { trigger: 'blur' }).catch(() => {})
"
/>
</a-form-item>
<a-form-item label="派单标识" v-bind="validateInfos.customerCode">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入社会统一信用代码"
v-model:value="form.customerCode"
/>
</a-form-item>
<a-form-item label="联系人" v-bind="validateInfos.customerContacts">
<a-input
allow-clear
:maxlength="20"
placeholder="请填写联系人"
v-model:value="form.customerContacts"
/>
</a-form-item>
<a-form-item label="手机号码" v-bind="validateInfos.customerMobile">
<a-input
allow-clear
:maxlength="20"
placeholder="请填写联系人手机号码"
v-model:value="form.customerMobile"
/>
</a-form-item>
<a-form-item label="头像" v-bind="validateInfos.customerAvatar">
<ele-image-upload
v-model:value="images"
:item-style="{ width: '90px', height: '90px' }"
:limit="1"
@upload="onUpload"
/>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="派单全称" v-bind="validateInfos.customerFullName">
<a-input
allow-clear
:maxlength="30"
placeholder="请输入派单全称"
v-model:value="form.customerFullName"
@blur="
validate('customerFullName', { trigger: 'blur' }).catch(() => {})
"
/>
</a-form-item>
<a-form-item
label="联系地址"
v-bind="validateInfos.customerAddress"
>
<a-input
allow-clear
placeholder="请填写联系地址"
v-model:value="form.customerAddress"
/>
</a-form-item>
<a-form-item label="公司座机" v-bind="validateInfos.customerPhone">
<a-input
allow-clear
:maxlength="20"
placeholder="请填写公司座机电话"
v-model:value="form.customerPhone"
/>
</a-form-item>
<a-form-item label="排序" v-bind="validateInfos.sortNumber">
<a-input
allow-clear
:maxlength="20"
placeholder="排序"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="备注" v-bind="validateInfos.comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.comments"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import {ref, reactive, watch, computed} from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import { addCustomer, updateCustomer } from '@/api/oa/customer';
import type { Customer } from '@/api/oa/customer/model';
import { createCode } from '@/utils/common';
import { uploadFile } from '@/api/system/file';
import type { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FILE_SERVER } from '@/config/setting';
import { useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
// 当前用户信息
const loginUser = computed(() => userStore.info ?? {});
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Customer | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 用户信息
const form = reactive<Customer>({
customerCode: '',
customerName: '',
customerFullName: '',
customerType: undefined,
progress: undefined,
customerMobile: '',
customerAvatar: '',
customerPhone: '',
customerSource: '',
customerContacts: '',
customerAddress: '',
comments: '',
status: '0',
sortNumber: 100,
customerId: 0,
userId: '',
});
// 已上传数据, 可赋初始值用于回显
const images = ref(<any>[]);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
customerName: [
{
required: true,
type: 'string',
message: '请输入派单名称',
trigger: 'blur'
}
],
customerCode: [
{
required: true,
type: 'string',
message: '请输入合法的IP地址',
trigger: 'blur'
}
]
});
const { resetFields, validate, validateInfos } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
validate()
.then(() => {
loading.value = true;
// 去除空格
form.customerName = form.customerName?.replace(/\s*/g, '');
// 判断权限
// if (loginUser.value.roles?.[0].roleCode != 'admin'){
// form.status = '1';
// }
const data = {
...form
};
// 转字符串
const saveOrUpdate = isUpdate.value ? updateCustomer : addCustomer;
saveOrUpdate(data)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
// 上传文件
const onUpload = (d: ItemType) => {
uploadFile(<File>d.file)
.then((result) => {
form.customerAvatar = result.path;
message.success('上传成功');
})
.catch((e) => {
message.error(e.message);
});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
loading.value = false;
// 头像赋值
images.value = [];
if(props.data.customerAvatar){
images.value.push({ uid:1, url: FILE_SERVER + props.data.customerAvatar, status: '' });
}
assignObject(form, props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>
<style lang="less"></style>
@@ -0,0 +1,148 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
width="75%"
:visible="visible"
:confirm-loading="loading"
:title="'派单详情'"
:maxable="true"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
:footer="null"
>
<a-form
:label-col="{ md: { span: 4 }, sm: { span: 24 } }"
:wrapper-col="{ md: { span: 19 }, sm: { span: 24 } }"
>
<div class="base-form" style="margin-bottom: 20px">
<a-descriptions bordered>
<a-descriptions-item label="派单名称">
{{ customer.customerName }}
</a-descriptions-item>
<a-descriptions-item label="社会统一信用代码">
{{ customer.customerCode }}
</a-descriptions-item>
<a-descriptions-item label="跟进状态">
<div color="blue" v-for="(d, index) in progress" :key="index">
<span v-if="d.value == customer.progress">{{ d.label }}</span>
</div>
</a-descriptions-item>
<a-descriptions-item label="联系人">
{{ customer.customerContacts }}
</a-descriptions-item>
<a-descriptions-item label="联系电话">
{{ customer.customerMobile }}
</a-descriptions-item>
<a-descriptions-item label="座机电话">
{{ customer.customerPhone }}
</a-descriptions-item>
<a-descriptions-item label="派单类型">
<div color="blue" v-for="(d, index) in customerType" :key="index">
<span v-if="d.value == customer.customerType">{{ d.value }}</span>
</div>
</a-descriptions-item>
<a-descriptions-item label="联系地址">
{{ customer.customerAddress }}
</a-descriptions-item>
<a-descriptions-item label="备注">
{{ customer.comments }}
</a-descriptions-item>
</a-descriptions>
<!-- <a-descriptions-->
<!-- title="其他信息"-->
<!-- :column="1"-->
<!-- bordered-->
<!-- style="margin-top: 30px"-->
<!-- >-->
<!-- <a-descriptions-item label="相关项目">-->
<!-- {{ customer.comments }}-->
<!-- </a-descriptions-item>-->
<!-- </a-descriptions>-->
</div>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import type { Customer } from '@/api/oa/customer/model';
import { FILE_SERVER } from '@/config/setting';
import { getDictionaryOptions } from '@/utils/common';
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Customer | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 用户信息
const customer = reactive<Customer>({
customerCode: '',
customerName: '',
customerType: undefined,
customerMobile: '',
customerAvatar: '',
customerPhone: '',
customerContacts: '',
customerAddress: '',
comments: '',
progress: '',
status: '0',
sortNumber: 100,
customerId: 0
});
// 请求状态
const loading = ref(true);
const { resetFields } = useForm(customer);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
/* 打开外部链接 */
// const openUrl = (record) => {
// window.open(record.panel);
// };
/* 获取字典数据 */
const customerType = getDictionaryOptions('customerType');
const progress = getDictionaryOptions('customerFollowStatus');
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
loading.value = false;
assignObject(customer, props.data);
}
} else {
resetFields();
}
}
);
</script>
<style lang="less">
.tab-pane {
min-height: 100px;
}
.card-head {
display: flex;
height: 40px;
align-items: center;
margin-bottom: 30px;
}
</style>
@@ -0,0 +1,206 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>新增</span>
</a-button>
<a-button
danger
type="primary"
class="ele-btn-icon"
v-if="selection.length > 0"
@click="removeBatch"
>
<template #icon>
<DeleteOutlined />
</template>
<span>批量删除</span>
</a-button>
<!-- <a-button @click="batchMove" v-if="selection.length > 0">-->
<!-- <template #icon>-->
<!-- <UserSwitchOutlined />-->
<!-- </template>-->
<!-- 批量转移-->
<!-- </a-button>-->
<a-input-search
allow-clear
placeholder="请输入关键词"
v-model:value="searchText"
@pressEnter="search"
@search="search"
/>
</a-space>
</template>
<script lang="ts" setup>
import {
PlusOutlined,
DeleteOutlined,
UploadOutlined,
DownloadOutlined
} from '@ant-design/icons-vue';
import useSearch from '@/utils/use-search';
import type { CustomerParam } from '@/api/oa/customer/model';
import { ref, watch } from 'vue';
import { utils, read } from 'xlsx';
// import { assignObject } from 'ele-admin-pro';
import { message } from 'ant-design-vue/es';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: CustomerParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 表单数据
const { where } = useSearch<CustomerParam>({
customerName: '',
customerCode: '',
nickname: '',
keywords: '',
userId: undefined
});
// 下来选项
// 搜索内容
const searchText = ref('');
/* 搜索 */
const search = () => {
where.keywords = searchText.value;
emit('search', where);
};
// 新增
const add = () => {
emit('add');
};
// 导入数据的列
const importTitle = ref<string[]>(['A', 'B', 'C', 'D', 'E', 'F', 'G']);
// 导入的数据
const importData = ref<Record<string, any>[]>([]);
// 导入数据二维数组形式
const importDataAoa = ref<(string | number)[][]>([]);
/* 导入本地 excel 文件 */
const importBatch = (file: File) => {
if (
![
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
].includes(file.type)
) {
message.error('只能选择 excel 文件');
return false;
}
if (file.size / 1024 / 1024 > 20) {
message.error('大小不能超过 20MB');
return false;
}
const reader = new FileReader();
reader.onload = (e) => {
const data = new Uint8Array(e.target?.result as any);
const workbook = read(data, { type: 'array' });
const sheetNames = workbook.SheetNames;
const worksheet = workbook.Sheets[sheetNames[0]];
// 解析成二维数组
const aoa = utils.sheet_to_json<string[]>(worksheet, { header: 1 });
// 生成表格需要的数据
let list: Record<string, any>[] = [];
let maxCols = 0;
let title: string[] = [];
aoa.forEach((d) => {
if (d.length > maxCols) {
maxCols = d.length;
}
const row = {};
for (let i = 0; i < d.length; i++) {
const key = getCharByIndex(i);
row[key] = d[i];
row['__colspan__' + key] = 1;
row['__rowspan__' + key] = 1;
}
list.push(row);
});
for (let i = 0; i < maxCols; i++) {
title.push(getCharByIndex(i));
}
importTitle.value = title;
importData.value = list;
importDataAoa.value = aoa;
};
console.log(importData.value);
console.log(importDataAoa.value);
reader.readAsArrayBuffer(file);
return false;
};
/* 生成Excel列字母序号 */
const getCharByIndex = (index: number) => {
const chars = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z'
];
if (index < chars.length) {
return chars[index];
}
const n = parseInt(String(index / chars.length));
const m = index % chars.length;
return chars[n] + chars[m];
};
// 转移
// const batchMove = () => {
// emit('batchMove');
// };
// 批量删除
const removeBatch = () => {
emit('remove');
};
const onClear = () => {
where.userId = undefined;
search();
};
watch(
() => props.selection,
() => {}
);
</script>
@@ -0,0 +1,322 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="customerId"
:columns="columns"
:datasource="datasource"
v-model:selection="selection"
tool-class="ele-toolbar-form"
:scroll="{ x: 800 }"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'customerName'">
<a-avatar
:size="30"
:src="`${record.customerAvatar}`"
style="margin-right: 4px"
:srcset="`https://file.wsdns.cn/${record.customerAvatar}`"
>
<template #icon>
<UserOutlined />
</template>
</a-avatar>
<a-tooltip title="查看详情">
<a href="#" @click="openInfo(record)">{{
record.customerName
}}</a>
</a-tooltip>
</template>
<template v-if="column.key === 'progress'">
<div v-for="(d, i) in progressDict" :key="i">
<span v-if="d.value === record.progress">{{ d.label }}</span>
</div>
</template>
<template v-if="column.key === 'customerType'">
<div v-for="(d, i) in JSON.parse(customerType)" :key="i">
<span v-if="d.value === record.customerType">{{
d.value
}}</span>
</div>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">正常</a-tag>
<a-tag v-if="record.status === 1" color="red">待审核</a-tag>
<a-tag v-if="record.status === 2" color="purple">已驳回</a-tag>
</template>
<template v-if="column.key === 'nickname'">
<a-tooltip :title="`${record.nickname}`">
<a-avatar :src="record.userAvatar" size="small" />
</a-tooltip>
</template>
<template v-if="column.key === 'createTime'">
<a-tooltip :title="`${toDateString(record.createTime)}`">
{{ timeAgo(record.createTime) }}
</a-tooltip>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<CustomerEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 派单详情弹窗 -->
<CustomerInfo v-model:visible="showInfo" :data="current" @done="reload" />
<!-- 批量转移弹窗 -->
<!-- <CustomerMove v-model:visible="showMove" :data="selection" @done="batchMove" />-->
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import {
ExclamationCircleOutlined,
UserOutlined
} from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { toDateString } from 'ele-admin-pro';
import Search from './components/search.vue';
import CustomerEdit from './components/customer-edit.vue';
import CustomerInfo from './components/customer-info.vue';
import {
pageCustomer,
removeCustomer,
removeBatchCustomer
} from '@/api/oa/customer';
import { timeAgo } from 'ele-admin-pro';
import type { Customer, CustomerParam } from '@/api/oa/customer/model';
import { useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
// 当前用户信息
const loginUser = computed(() => userStore.info ?? {});
const customerType = localStorage.getItem('customerType');
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<Customer[]>([]);
// 当前编辑数据
const current = ref<Customer | null>(null);
// 是否显示资产详情
const showInfo = ref(false);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.progress = filters.progress;
where.customerSource = filters.customerSource;
where.customerType = filters.customerType;
where.status = filters.status;
}
return pageCustomer({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '状态',
dataIndex: 'contractNo',
key: 'contractNo',
ellipsis: true
},
{
title: '派单人',
dataIndex: 'contractNo'
},
{
title: '项目名称',
dataIndex: 'contractNo'
},
{
title: '楼号',
dataIndex: 'contractNo'
},
{
title: '设备名称',
dataIndex: 'contractNo'
},
{
title: '作业类型',
dataIndex: 'contractNo'
},
{
title: '班组负责人',
dataIndex: 'contractNo'
},
{
title: '作业人员',
dataIndex: 'contractNo'
},
{
title: '派单时间',
dataIndex: 'contractNo'
},
{
title: '计划进场',
dataIndex: 'contractNo'
},
{
title: '实际进场',
dataIndex: 'contractNo'
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: CustomerParam) => {
console.log(where);
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: Customer) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 打开用户详情弹窗 */
const openInfo = (row?: Customer) => {
current.value = row ?? null;
showInfo.value = true;
};
/* 删除单个 */
const remove = (row: Customer) => {
const hide = message.loading('请求中..', 0);
removeCustomer(row.customerId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量转移 */
const batchMove = (userId) => {
console.log(userId, '批量转移0000');
console.log(selection.value);
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchCustomer(
selection.value.map((d) => {
if (loginUser.value.userId === d.userId) {
return d.customerId;
}
})
)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
</script>
<script lang="ts">
export default {
name: 'Accessory'
};
</script>
<style lang="less" scoped>
.sys-org-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.sys-org-table :deep(.ant-table-pagination.ant-pagination) {
padding: 0 4px;
margin-bottom: 0;
}
</style>
@@ -0,0 +1,697 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
width="80%"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑设备' : '添加设备'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<div style="background-color: #f3f3f3; padding: 8px">
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ md: { span: 7 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 17 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<a-card title="基本信息" :bordered="false">
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="设备名称" name="name">
<DictSelect
dict-code="DeviceAssignment"
placeholder="请选择设备"
v-model:value="form.name"
/>
</a-form-item>
<a-form-item label="出厂编号" name="factoryNo">
<a-input
allow-clear
placeholder="请输入出厂编号"
v-model:value="form.factoryNo"
/>
</a-form-item>
<a-form-item label="自编号" name="equipmentNo">
<a-input
allow-clear
placeholder="请输入设备编号"
v-model:value="form.equipmentNo"
/>
</a-form-item>
<a-form-item label="剩余使用年限" name="surplusYear">
<a-input
allow-clear
placeholder="请输入插件名称"
disabled
v-model:value="form.surplusYear"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="设备型号" name="model">
<DictSelect
dict-code="EquipmentModel"
placeholder="请选择设备型号"
v-model:value="form.model"
/>
</a-form-item>
<a-form-item label="出厂日期" name="factoryDate">
<a-date-picker
class="ele-fluid"
value-format="YYYY-MM-DD"
placeholder="请选择日期"
v-model:value="form.factoryDate"
/>
</a-form-item>
<a-form-item label="使用年限" name="lifeYear">
<a-input
allow-clear
placeholder="请输入使用年限"
v-model:value="form.lifeYear"
@change="onLifeYaer"
/>
</a-form-item>
<a-form-item label="产权单位" name="company">
<TowerCompany
:placeholder="`请选择租赁单位`"
v-model:value="form.company"
:customer-type="`产权单位`"
@done="chooseCompanyName"
/>
<!-- <DictSelect-->
<!-- dict-code="propertyCompany"-->
<!-- placeholder="请选择产权单位"-->
<!-- v-model:value="form.company"-->
<!-- />-->
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="备案编号" name="filingNo">
<a-input
allow-clear
placeholder="请输入备案编号"
v-model:value="form.filingNo"
/>
</a-form-item>
<a-form-item label="制造厂家" name="manufactor">
<DictSelect
dict-code="Manufacturer"
placeholder="请选择制造厂家"
v-model:value="form.manufactor"
/>
</a-form-item>
<a-form-item label="报废日期" name="scrapDate">
<a-date-picker
class="ele-fluid"
value-format="YYYY-MM-DD"
placeholder="请选择日期"
v-model:value="form.scrapDate"
/>
</a-form-item>
<a-form-item label="备案许可证号" name="licenceNo">
<a-input
allow-clear
placeholder="请输入插件名称"
v-model:value="form.licenceNo"
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
<a-card title="设备信息" :bordered="false">
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="所属仓库" name="warehouse">
<SelectWarehouse
:placeholder="`请选择仓库`"
v-model:value="form.warehouse"
@done="chooseWarehouse"
/>
</a-form-item>
<a-form-item label="采购日期" name="title">
<a-date-picker
class="ele-fluid"
value-format="YYYY-MM-DD"
placeholder="请选择日期"
v-model:value="form.buyDate"
/>
</a-form-item>
<a-form-item label="当前位置" name="currentLocation">
<a-input
allow-clear
disabled
placeholder="请输入当前位置"
v-model:value="form.currentLocation"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="所属人员" name="users">
<SelectUser
:placeholder="`请选择所属人员`"
v-model:value="form.users"
@done="chooseUsers"
/>
</a-form-item>
<a-form-item label="安全管理部门" name="organization">
<SelectOrganization
:placeholder="`请选择所属部门`"
v-model:value="form.organization"
@done="chooseOrganization"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="设备来源" name="source">
<a-input
allow-clear
placeholder="请输入设备来源"
v-model:value="form.source"
/>
</a-form-item>
<a-form-item label="机号" name="machineNo">
<a-input
allow-clear
placeholder="请输入插件名称"
v-model:value="form.machineNo"
/>
</a-form-item>
<a-form-item label="是否外协标识" name="isOutOrg">
<a-switch v-model:checked="form.isOutOrg" />
</a-form-item>
</a-col>
</a-row>
</a-card>
<a-card title="设备参数" :bordered="false">
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="设计高度" name="designHeight">
<a-input-number
allow-clear
:min="0"
style="width: 200px"
placeholder="请输入设计高度"
v-model:value="form.designHeight"
/>
<span class="ml-10">m</span>
</a-form-item>
<a-form-item label="独立高度" name="height">
<a-input-number
allow-clear
:min="0"
style="width: 200px"
placeholder="请输入独立高度"
v-model:value="form.height"
/>
<span class="ml-10">m</span>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 16 }
"
>
<a-form-item label="额定载重" name="ratedLoad">
<a-input-number
allow-clear
:min="0"
style="width: 200px"
placeholder="请输入额定载重"
v-model:value="form.ratedLoad"
/>
<span class="ml-10">T</span>
</a-form-item>
<a-form-item label="最大幅度处额定载重" name="maxRatedLoad">
<a-input-number
allow-clear
:min="0"
style="width: 200px"
placeholder="请输入最大幅度处额定载重"
v-model:value="form.maxRatedLoad"
/>
<span class="ml-10">T</span>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 6, sm: 24, xs: 24 } : { span: 8 }"
/>
</a-row>
</a-card>
<a-form
:label-col="{ md: { span: 2 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{
md: { span: 17 },
sm: { span: 20 },
xs: { span: 24 }
}"
>
<a-card title="附件信息" :bordered="false">
<a-form-item label="采购合同" name="parentId">
<a-upload
v-model:file-list="file1"
name="file"
action="https://file.wsdns.cn/api/file/image"
:headers="{ Authorization: token }"
@change="onFile1"
>
<a-button>
<upload-outlined />
上传
</a-button>
</a-upload>
</a-form-item>
<a-form-item label="发票" name="parentId">
<a-upload
v-model:file-list="file2"
name="file"
action="https://file.wsdns.cn/api/file/image"
:headers="{ Authorization: token }"
@change="onFile2"
>
<a-button>
<upload-outlined />
上传
</a-button>
</a-upload>
</a-form-item>
<a-form-item label="制造许可证" name="parentId">
<a-upload
v-model:file-list="file3"
name="file"
action="https://file.wsdns.cn/api/file/image"
:headers="{ Authorization: token }"
@change="onFile3"
>
<a-button>
<upload-outlined />
上传
</a-button>
</a-upload>
</a-form-item>
<a-form-item label="出厂合格证" name="parentId">
<a-upload
v-model:file-list="file4"
name="file"
action="https://file.wsdns.cn/api/file/image"
:headers="{ Authorization: token }"
@change="onFile4"
>
<a-button>
<upload-outlined />
上传
</a-button>
</a-upload>
</a-form-item>
<a-form-item label="备案证书" name="parentId">
<a-upload
v-model:file-list="file5"
name="file"
action="https://file.wsdns.cn/api/file/image"
:headers="{ Authorization: token }"
@change="onFile5"
>
<a-button>
<upload-outlined />
上传
</a-button>
</a-upload>
</a-form-item>
<a-form-item label="整机性能参数表" name="parentId">
<a-upload
v-model:file-list="file6"
name="file"
action="https://file.wsdns.cn/api/file/image"
:headers="{ Authorization: token }"
@change="onFile6"
>
<a-button>
<upload-outlined />
上传
</a-button>
</a-upload>
</a-form-item>
<a-form-item label="其他" name="parentId">
<a-upload
v-model:file-list="file7"
name="file"
action="https://file.wsdns.cn/api/file/image"
:headers="{ Authorization: token }"
@change="onFile7"
>
<a-button>
<upload-outlined />
上传
</a-button>
</a-upload>
</a-form-item>
</a-card>
</a-form>
</a-form>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import {
addTowerEquipment,
updateTowerEquipment
} from '@/api/tower/equipment';
import { TowerEquipment } from '@/api/tower/equipment/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { UploadOutlined } from '@ant-design/icons-vue';
import DictSelect from '@/views/search/components/dict-select.vue';
import { FormInstance } from 'ant-design-vue/es/form';
import { TOKEN_STORE_NAME } from '@/config/setting';
import { Warehouse } from '@/api/tower/warehouse/model';
import { User } from '@/api/user/model';
import { Organization } from '@/api/system/organization/model';
import { Customer } from '@/api/oa/customer/model';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: TowerEquipment | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
const formRef = ref<FormInstance | null>(null);
interface FileItem {
uid: string;
name?: string;
status?: string;
response?: Response;
thumbUrl?: string;
downloadUrl?: string;
url: string;
}
interface FileInfo {
file: FileItem;
fileList: FileItem[];
}
// 文件上传
const file1 = ref<FileItem[]>();
const file2 = ref<FileItem[]>();
const file3 = ref<FileItem[]>();
const file4 = ref<FileItem[]>();
const file5 = ref<FileItem[]>();
const file6 = ref<FileItem[]>();
const file7 = ref<FileItem[]>();
// 日期范围选择
const token = localStorage.getItem(TOKEN_STORE_NAME);
// 用户信息
const form = reactive<TowerEquipment>({
equipmentId: undefined,
name: undefined,
model: undefined,
equipmentNo: '',
factoryNo: '',
factoryDate: '',
manufactor: undefined,
company: '',
lifeYear: undefined,
surplusYear: 3,
scrapDate: '',
licenceNo: '',
organization: undefined,
warehouse: undefined,
source: undefined,
buyDate: '',
currentLocation: '',
designHeight: undefined,
height: undefined,
ratedLoad: '',
files: '[]',
file1: '[]',
file2: '[]',
file3: '[]',
file4: '[]',
file5: '[]',
file6: '[]',
file7: '[]',
filingNo: '',
machineNo: '',
users: '',
isOutOrg: false,
status: undefined,
comments: '',
userId: undefined,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
maxRatedLoad: ''
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
name: [
{
required: true,
type: 'string',
message: '请输入设备名称',
trigger: 'blur'
}
],
model: [
{
required: true,
type: 'string',
message: '请输入设备型号',
trigger: 'blur'
}
]
});
const { resetFields } = useForm(form, rules);
const onLifeYaer = () => {
form.surplusYear = form.lifeYear;
};
// const onFactoryDate = () => {
// form.scrapDate = form.factoryDate;
// };
const onFile1 = (info: FileInfo) => {
let resFileList = [...info.fileList];
file1.value = resFileList.map((file) => {
if (file.response) {
// Component will show file.url as link
file.url = file.response.url;
}
return file;
});
form.file1 = JSON.stringify(file1.value);
};
const onFile2 = (info: FileInfo) => {
let resFileList = [...info.fileList];
file2.value = resFileList.map((file) => {
if (file.response) {
// Component will show file.url as link
file.url = file.response.url;
}
return file;
});
form.file2 = JSON.stringify(file2.value);
};
const onFile3 = (info: FileInfo) => {
let resFileList = [...info.fileList];
file3.value = resFileList.map((file) => {
if (file.response) {
// Component will show file.url as link
file.url = file.response.url;
}
return file;
});
form.file3 = JSON.stringify(file3.value);
};
const onFile4 = (info: FileInfo) => {
let resFileList = [...info.fileList];
file4.value = resFileList.map((file) => {
if (file.response) {
// Component will show file.url as link
file.url = file.response.url;
}
return file;
});
form.file4 = JSON.stringify(file4.value);
};
const onFile5 = (info: FileInfo) => {
let resFileList = [...info.fileList];
file5.value = resFileList.map((file) => {
if (file.response) {
// Component will show file.url as link
file.url = file.response.url;
}
return file;
});
form.file5 = JSON.stringify(file5.value);
};
const onFile6 = (info: FileInfo) => {
let resFileList = [...info.fileList];
file6.value = resFileList.map((file) => {
if (file.response) {
// Component will show file.url as link
file.url = file.response.url;
}
return file;
});
form.file6 = JSON.stringify(file6.value);
};
const onFile7 = (info: FileInfo) => {
let resFileList = [...info.fileList];
file7.value = resFileList.map((file) => {
if (file.response) {
// Component will show file.url as link
file.url = file.response.url;
}
return file;
});
form.file7 = JSON.stringify(file7.value);
};
const chooseWarehouse = (data: Warehouse) => {
console.log(data);
form.warehouse = data.warehouseName;
form.currentLocation = data.address;
};
const chooseUsers = (data: User) => {
form.users = data.nickname;
};
const chooseOrganization = (data: Organization) => {
form.organization = data.organizationName;
};
const chooseCompanyName = (data: Customer) => {
form.company = data.customerName;
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value
? updateTowerEquipment
: addTowerEquipment;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignObject(form, props.data);
if (props.data.file1) {
file1.value = JSON.parse(props.data.file1);
}
if (props.data.file2) {
file2.value = JSON.parse(props.data.file2);
}
if (props.data.file3) {
file3.value = JSON.parse(props.data.file3);
}
if (props.data.file4) {
file4.value = JSON.parse(props.data.file4);
}
if (props.data.file5) {
file5.value = JSON.parse(props.data.file5);
}
if (props.data.file6) {
file6.value = JSON.parse(props.data.file6);
}
if (props.data.file7) {
file7.value = JSON.parse(props.data.file7);
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>
<style lang="less"></style>
<style lang="less">
.tab-pane {
min-height: 300px;
}
.ml-10 {
margin-left: 5px;
}
.upload-text {
margin-right: 70px;
}
</style>
@@ -0,0 +1,95 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon">
<template #icon>
<QrcodeOutlined />
</template>
<span>批量生产二维码</span>
</a-button>
<a-alert type="success" style="margin-left: 105px">
<template #icon><smile-outlined /></template>
<template #message>
<a-space>
<span>设备总数:4</span>
<span>在用数:1</span>
<span>闲置数:1</span>
<span>报废数:1</span>
<span>转卖数:1</span>
<span>注销数:0</span>
<span>自有设备:3</span>
<span>租赁设备:1</span>
</a-space>
</template>
</a-alert>
</a-space>
</template>
<script lang="ts" setup>
import { QrcodeOutlined } from '@ant-design/icons-vue';
import useSearch from '@/utils/use-search';
import { ref, watch } from 'vue';
import { TowerEquipmentParam } from '@/api/tower/equipment/model';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: TowerEquipmentParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'update', status?: number): void;
(e: 'advanced'): void;
}>();
// 表单数据
const { where, resetFields } = useSearch<TowerEquipmentParam>({
equipmentId: undefined,
name: undefined,
keywords: ''
});
// 下来选项
const type = ref('keywords');
// 搜索内容
const searchText = ref('');
// 日期范围选择
// const dateRange = ref<[string, string]>(['', '']);
// 状态
const status = ref<number>();
/* 搜索 */
const search = () => {
resetFields();
// const [d1, d2] = dateRange.value ?? [];
if (type.value == 'keywords') {
where.keywords = searchText.value;
}
emit('search', {
...where
});
};
// 发布设备
const add = () => {
emit('add');
};
const handleStatus = () => {
emit('update', status.value);
};
// 批量删除
const removeBatch = () => {
emit('remove');
};
watch(
() => props.selection,
() => {}
);
</script>
+281
View File
@@ -0,0 +1,281 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="equipmentId"
:columns="columns"
:datasource="datasource"
v-model:selection="selection"
:customRow="customRow"
tool-class="ele-toolbar-form"
:scroll="{ x: 1200 }"
class="sys-org-table"
:striped="true"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@update="handleStatus"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'nickname'">
{{ record.nickname }}
</template>
<template v-if="column.key === 'createTime'">
{{ record.createTime }}
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">闲置</a-tag>
<a-tag v-if="record.status === 1" color="red">报废</a-tag>
<a-tag v-if="record.status === 2" color="purple">注销</a-tag>
<a-tag v-if="record.status === 3" color="orange">转卖</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a>一机一档</a>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
</div>
<!-- 编辑弹窗 -->
<EquipmentEdit v-model:visible="showEdit" :data="current" @done="reload" />
</div>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { toDateString } from 'ele-admin-pro';
import Search from './components/search.vue';
import EquipmentEdit from './components/equipment-edit.vue';
import {
pageTowerEquipment,
removeBatchTowerEquipment,
statusBatchTowerEquipment, updateBatchTowerEquipment,
updateTowerEquipment
} from "@/api/tower/equipment";
import {
TowerEquipment,
TowerEquipmentParam
} from '@/api/tower/equipment/model';
defineProps<{
activeKey?: boolean;
data?: any;
}>();
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '设备状态',
dataIndex: 'status',
key: 'status',
customRender: ({ text }) => ['闲置', '报废', '注销', '转卖'][text]
},
{
title: '设备名称',
dataIndex: 'name',
key: 'name'
},
{
title: '设备型号',
dataIndex: 'model',
key: 'model'
},
{
title: '制造厂商',
dataIndex: 'manufactor',
key: 'manufactor'
},
{
title: '当前位置',
dataIndex: 'currentLocation',
key: 'currentLocation'
},
{
title: '产权单位(个人)',
dataIndex: 'company',
key: 'company'
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
customRender: ({ text }) => toDateString(text)
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center',
hideInSetting: true
}
]);
// 表格选中数据
const selection = ref<TowerEquipment[]>([]);
// 当前编辑数据
const current = ref<TowerEquipment | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
// 搜索条件
if (filters.payMethod) {
where.payMethod = filters.payMethod;
}
if (filters.deliveryType) {
where.deliveryType = filters.deliveryType;
}
if (filters.payStatus) {
where.payStatus = filters.payStatus;
}
if (filters.orderSource) {
where.orderSource = filters.orderSource;
}
where.tenantId = localStorage.getItem('tenantId');
where.payStatus = 20;
return pageTowerEquipment({
...where,
...orders,
page,
limit
});
};
/* 搜索 */
const reload = (where?: TowerEquipmentParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: TowerEquipment) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 批量修改 */
const handleStatus = (status) => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
const data = selection.value.map((d) => {
return {
equipmentId: d.equipmentId,
status
};
});
console.log(data);
updateBatchTowerEquipment(data).then((msg) => {
message.success(msg);
reload();
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchTowerEquipment(selection.value.map((d) => d.equipmentId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 自定义行属性 */
const customRow = (record: TowerEquipment) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
reload();
</script>
<script lang="ts">
export default {
name: 'TowerEquipmentIndex'
};
</script>
<style lang="less" scoped>
p {
line-height: 0.8;
}
.sys-org-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.sys-org-table :deep(.ant-table-pagination.ant-pagination) {
padding: 0 4px;
margin-bottom: 0;
}
.price-edit {
padding-right: 5px;
}
.comments {
max-width: 200px;
}
</style>
@@ -0,0 +1,249 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="740"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '修改分类' : '新建分类'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 6, sm: 4, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 18, sm: 20, xs: 24 } : { flex: '1' }
"
>
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="上级分类" name="parentId">
<a-tree-select
allow-clear
:tree-data="categoryList"
tree-default-expand-all
placeholder="请选择上级分类"
:value="form.parentId || undefined"
:dropdown-style="{ maxHeight: '360px', overflow: 'auto' }"
@update:value="(value?: number) => (form.parentId = value)"
/>
</a-form-item>
<a-form-item label="设备型号" name="title">
<a-input
allow-clear
placeholder="请输入分类名称"
v-model:value="form.title"
@pressEnter="save"
/>
</a-form-item>
<a-form-item label="使用年限" name="yearLife">
<a-input
allow-clear
placeholder="请输入分类名称"
v-model:value="form.yearLife"
@pressEnter="save"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import type { TowerModel } from '@/api/tower/model/model';
import { addTowerModel, updateTowerModel } from '@/api/tower/model';
import { FILE_SERVER } from "@/config/setting";
import { ItemType } from "ele-admin-pro/es/ele-image-upload/types";
import { uploadFile } from "@/api/system/file";
import { LoginRecordParam } from "@/api/system/login-record/model";
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: TowerModel | null;
// 上级分类id
parentId?: number;
// 全部分类数据
categoryList: TowerModel[];
}>();
// 表单数据
const { form, resetFields, assignFields } = useFormData<TowerModel>({
title: '',
categoryId: undefined,
parentId: undefined,
image: '',
status: 0,
yearLife: undefined,
sortNumber: 100
});
//
const formRef = ref<FormInstance | null>(null);
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 已上传数据, 可赋初始值用于回显
const images = ref([]);
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
title: [
{
required: true,
message: '请输入分类名称',
type: 'string',
trigger: 'blur'
}
],
sortNumber: [
{
required: true,
message: '请输入排序号',
type: 'number',
trigger: 'blur'
}
],
meta: [
{
type: 'string',
validator: async (_rule: Rule, value: string) => {
if (value) {
const msg = '请输入正确的JSON格式';
try {
const obj = JSON.parse(value);
if (typeof obj !== 'object' || obj === null) {
return Promise.reject(msg);
}
} catch (_e) {
return Promise.reject(msg);
}
}
return Promise.resolve();
},
trigger: 'blur'
}
]
});
// 上传文件
const onUpload = (d: ItemType) => {
uploadFile(<File>d.file)
.then((result) => {
form.image = result.path;
message.success('上传成功');
})
.catch((e) => {
message.error(e.message);
});
};
const onClose = () => {
form.image = ''
}
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const categoryForm = {
...form,
parentId: form.parentId || 0
};
const saveOrUpdate = isUpdate.value ? updateTowerModel : addTowerModel;
saveOrUpdate(categoryForm)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const updateHideValue = (value: boolean) => {
form.status = value ? 0 : 1;
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
// 头像赋值
images.value = [];
if(props.data.image){
images.value.push({ uid:1, url: props.data.image, status: '' });
}
assignFields({
...props.data,
parentId:
props.data.parentId === 0 ? undefined : props.data.parentId
});
isUpdate.value = true;
} else {
form.parentId = props.parentId;
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>
<script lang="ts">
import * as icons from '@/layout/menu-icons';
export default {
components: icons,
data() {
return {
iconData: [
{
title: '已引入的图标',
icons: Object.keys(icons)
}
]
};
}
};
</script>
+288
View File
@@ -0,0 +1,288 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="categoryId"
:columns="columns"
:datasource="datasource"
:parse-data="parseData"
:need-page="false"
:expand-icon-column-index="1"
:expanded-row-keys="expandedRowKeys"
:scroll="{ x: 1200 }"
cache-key="proGoodsCategoryTable"
@done="onDone"
@expand="onExpand"
>
<template #toolbar>
<a-space>
<a-button type="primary" class="ele-btn-icon" @click="openEdit()">
<template #icon>
<plus-outlined />
</template>
<span>新建</span>
</a-button>
<a-button type="dashed" class="ele-btn-icon" @click="expandAll">
展开全部
</a-button>
<a-button type="dashed" class="ele-btn-icon" @click="foldAll">
折叠全部
</a-button>
<!-- 搜索表单 -->
<a-input-search
allow-clear
v-model:value="searchText"
placeholder="请输入搜索关键词"
@search="search"
@pressEnter="search"
/>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'menuType'">
<a-tag v-if="isExternalLink(record.path)" color="red">外链</a-tag>
<a-tag v-else-if="isExternalLink(record.component)" color="orange">
内链
</a-tag>
<a-tag v-else-if="isDirectory(record)" color="blue">目录</a-tag>
<a-tag v-else-if="record.menuType === 0" color="green">分类</a-tag>
<a-tag v-else-if="record.menuType === 1">按钮</a-tag>
</template>
<template v-else-if="column.key === 'title'">
<a-avatar
:size="20"
v-if="record.image"
shape="square"
:src="`${record.image}`"
/>
<a-tooltip :title="`分类ID:${record.categoryId}`">
<span>{{ record.title }}</span>
</a-tooltip>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="orange">隐藏</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a @click="openEdit(null, record.categoryId)">添加</a>
<a-divider type="vertical" />
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
placement="topRight"
title="确定要删除此分类吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<category-edit
v-model:visible="showEdit"
:data="current"
:parent-id="parentId"
:category-list="categoryData"
@done="reload"
/>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue/es';
import { PlusOutlined } from '@ant-design/icons-vue';
import type {
DatasourceFunction,
ColumnItem,
EleProTableDone
} from 'ele-admin-pro/es/ele-pro-table/types';
import {
messageLoading,
toDateString,
isExternalLink,
toTreeData,
eachTreeData
} from 'ele-admin-pro/es';
import type { EleProTable } from 'ele-admin-pro/es';
import CategoryEdit from './components/category-edit.vue';
import type { TowerModel, TowerModelParam } from '@/api/tower/model/model';
import { listTowerModel, removeTowerModel } from '@/api/tower/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '设备型号',
key: 'title',
showSorterTooltip: false,
ellipsis: true
},
{
title: '使用年限',
dataIndex: 'yearLife',
key: 'yearLife',
showSorterTooltip: false,
ellipsis: true
},
{
title: '排序',
dataIndex: 'sortNumber',
sorter: true,
width: 180,
showSorterTooltip: false
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
sorter: true,
width: 180,
showSorterTooltip: false,
customRender: ({ text }) => ['显示', '隐藏'][text]
},
{
title: '创建时间',
dataIndex: 'createTime',
sorter: true,
showSorterTooltip: false,
ellipsis: true,
width: 180,
customRender: ({ text }) => toDateString(text)
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center'
}
]);
// 当前编辑数据
const current = ref<TowerModel | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 上级分类id
const parentId = ref<number>();
// 分类数据
const categoryData = ref<TowerModel[]>([]);
// 表格展开的行
const expandedRowKeys = ref<number[]>([]);
// 表格数据源
const datasource: DatasourceFunction = ({ where }) => {
return listTowerModel({ ...where });
};
/* 数据转为树形结构 */
const parseData = (data: TowerModel[]) => {
return toTreeData({
data: data.map((d) => {
return { ...d, key: d.categoryId, value: d.categoryId };
}),
idField: 'categoryId',
parentIdField: 'parentId'
});
};
/* 表格渲染完成回调 */
const onDone: EleProTableDone<TowerModel> = ({ data }) => {
categoryData.value = data;
};
/* 刷新表格 */
const reload = (where?: TowerModelParam) => {
tableRef?.value?.reload({ where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: TowerModel | null, id?: number) => {
current.value = row ?? null;
parentId.value = id;
showEdit.value = true;
};
const search = (searchText) => {
reload({ title: searchText });
};
/* 删除单个 */
const remove = (row: TowerModel) => {
if (row.children?.length) {
message.error('请先删除子节点');
return;
}
const hide = messageLoading('请求中..', 0);
removeTowerModel(row.categoryId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 展开全部 */
const expandAll = () => {
let keys: number[] = [];
eachTreeData(categoryData.value, (d) => {
if (d.children && d.children.length && d.categoryId) {
keys.push(d.categoryId);
}
});
expandedRowKeys.value = keys;
};
/* 折叠全部 */
const foldAll = () => {
expandedRowKeys.value = [];
};
/* 点击展开图标时触发 */
const onExpand = (expanded: boolean, record: TowerModel) => {
if (expanded) {
expandedRowKeys.value = [
...expandedRowKeys.value,
record.categoryId as number
];
} else {
expandedRowKeys.value = expandedRowKeys.value.filter(
(d) => d !== record.categoryId
);
}
};
/* 判断是否是目录 */
const isDirectory = (d: TowerModel) => {
return !!d.children?.length;
};
</script>
<script lang="ts">
export default {
name: 'TowerModel'
};
</script>
@@ -0,0 +1,376 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
:width="`80%`"
:visible="visible"
:confirm-loading="loading"
:maxable="maxAble"
:title="isUpdate ? '编辑订单' : '订单详情'"
:body-style="{ paddingBottom: '8px', background: '#f3f3f3' }"
@update:visible="updateVisible"
:maskClosable="false"
:footer="null"
@ok="save"
>
<!-- <a-card class="order-card" :bordered="false">-->
<!-- <a-steps-->
<!-- :current="active"-->
<!-- direction="horizontal"-->
<!-- :responsive="styleResponsive"-->
<!-- >-->
<!-- <template v-for="(item, index) in steps" :key="index">-->
<!-- <a-step-->
<!-- :title="item.title"-->
<!-- :description="timeAgo(item.description)"-->
<!-- />-->
<!-- </template>-->
<!-- </a-steps>-->
<!-- </a-card>-->
<a-card title="订单详情" class="order-card">
<!-- <a-space>-->
<!-- <a-button>发货</a-button>-->
<!-- <a-button>商家备注</a-button>-->
<!-- <a-button>打印小票</a-button>-->
<!-- </a-space>-->
<a-row :gutter="16">
<a-col
v-bind="
styleResponsive
? { xl: 8, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 8 }
"
>
<a-form-item label="订单号" name="orderId">
<span>{{ data.orderId }}</span>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 8, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 8 }
"
>
<a-form-item label="实付款金额" name="payPrice">
<span class="ele-text-warning"
>¥{{ formatNumber(data.payPrice) }}</span
>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 8, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 8 }
"
>
<a-form-item label="订单状态" name="orderStatus">
<a-tag>{{ data.payStatus === 20 ? '已下单' : '' }}</a-tag>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 8, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 8 }
"
>
<a-form-item label="买家信息" name="deliveryType">
<router-link :to="'/system/user/details?id=' + data.userId">
<span class="ele-text-primary">{{ data.nickname }}</span>
</router-link>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 8, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 8 }
"
>
<a-form-item label="预定日期" name="deliveryTime">
{{ toDateString(data.deliveryTime, 'yyyy-MM-dd') }}
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 8, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 8 }
"
>
<a-form-item label="下单时间" name="createTime">
{{ data.createTime }}
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 8, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 8 }
"
>
<a-form-item label="交易流水号" name="orderNo">
{{ data.orderNo }}
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 8, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 8 }
"
>
<a-form-item label="买家留言" name="buyerRemark">
<span>{{ data.buyerRemark }}</span>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 8, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 8 }
"
>
<a-form-item label="订单备注" name="comments">
<span>{{ data.comments }}</span>
</a-form-item>
</a-col>
</a-row>
</a-card>
<a-card title="菜品信息" class="order-card">
<a-spin :spinning="loading">
<a-table
:data-source="orderGoodsList"
:columns="columns"
:pagination="false"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'goodsName'">
<div class="goods-info">
<a-image
v-if="record.imageUrl"
:src="record.imageUrl"
:preview="false"
:width="50"
/>
<div class="info">
<div>{{ record.goodsName }}</div>
<div class="ele-text-placeholder">{{ record.comments }}</div>
</div>
</div>
</template>
</template>
</a-table>
</a-spin>
</a-card>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form } from 'ant-design-vue';
import { assignObject, toDateString } from 'ele-admin-pro';
import { useThemeStore } from '@/store/modules/theme';
import { formatNumber } from 'ele-admin-pro/es';
import { storeToRefs } from 'pinia';
import { Order } from '@/api/order/model';
import { ColumnItem } from 'ele-admin-pro/es/ele-pro-table/types';
import { OrderGoods } from '@/api/order/goods/model';
import { listOrderGoods } from '@/api/order/goods';
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Order | null;
}>();
export interface step {
title?: String | undefined;
subTitle?: String | undefined;
description?: String | undefined;
}
// 是否是修改
const isUpdate = ref(false);
// 是否显示最大化切换按钮
const maxAble = ref(true);
const orderGoodsList = ref<OrderGoods[]>([]);
// 步骤条
const steps = ref<step[]>([
{
title: '报餐',
description: undefined
},
{
title: '付款',
description: undefined
},
{
title: '发餐',
description: undefined
},
{
title: '取餐',
description: undefined
},
{
title: '完成',
description: undefined
}
]);
const active = ref(2);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 订单信息
const order = reactive<Order>({
orderId: undefined,
orderNo: '',
userId: undefined,
orderSourceData: ''
});
// 请求状态
const loading = ref(true);
const { resetFields } = useForm(order);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const columns = ref<ColumnItem[]>([
// {
// title: '菜品ID',
// dataIndex: 'goodsId'
// },
{
title: '菜品信息',
dataIndex: 'goodsName',
key: 'goodsName'
},
{
title: '菜品价格',
dataIndex: 'goodsPrice',
customRender: ({ text }) => '¥' + text
},
{
title: '购买数量',
dataIndex: 'totalNum',
key: 'totalNum'
}
]);
/* 制作步骤条 */
const loadSteps = (order) => {
steps.value = [];
steps.value.push({
title: '下单'
});
steps.value.push({
title: '付款'
});
steps.value.push({
title: '发货'
});
steps.value.push({
title: '收货'
});
steps.value.push({
title: '完成'
});
// 下单
if (order.payStatus == 10) {
active.value = 0;
steps.value[0].description = order.createTime;
}
// 付款
if (order.payStatus == 20) {
active.value = 1;
steps.value[0].description = order.createTime;
steps.value[1].description = order.payTime;
}
// 发货
if (order.payStatus == 20 && order.deliveryStatus == 20) {
active.value = 2;
steps.value[0].description = order.createTime;
steps.value[1].description = order.payTime;
steps.value[2].description = order.deliveryTime;
}
// 收货
if (order.payStatus == 20 && order.receiptStatus == 20) {
active.value = 3;
steps.value[0].description = order.createTime;
steps.value[1].description = order.payTime;
steps.value[2].description = order.deliveryTime;
steps.value[3].description = order.receiptTime;
}
// 完成
if (order.payStatus == 20 && order.orderStatus == 30) {
active.value = 4;
steps.value[0].description = order.createTime;
steps.value[1].description = order.payTime;
steps.value[2].description = order.deliveryTime;
steps.value[3].description = order.receiptTime;
}
// 已取消
if (order.orderStatus == 20) {
active.value = 4;
}
};
const getOrderGoods = () => {
const orderId = props.data?.orderId;
listOrderGoods({ orderId }).then((data) => {
orderGoodsList.value = data;
});
};
/* 保存编辑 */
const save = () => {};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
loading.value = false;
assignObject(order, props.data);
loadSteps(props.data);
getOrderGoods();
}
} else {
resetFields();
}
}
);
</script>
<style lang="less" scoped>
.order-card {
margin-bottom: 20px;
}
.ant-form-item {
margin-bottom: 5px;
}
.goods-info {
display: flex;
.info {
padding-left: 5px;
display: flex;
flex-direction: column;
}
}
</style>
@@ -0,0 +1,519 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
width="80%"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑项目' : '添加项目'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<div style="background-color: #f3f3f3; padding: 8px">
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ md: { span: 7 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 17 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<a-card title="项目信息" :bordered="false">
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<div class="upload-image">
<ele-image-upload
v-model:value="images"
:item-style="{ width: '360px', height: '240px' }"
:limit="1"
@upload="onUpload"
/>
</div>
<a-form-item label="主管部门" name="competentDepartment">
<a-input
allow-clear
placeholder="请输入主管部门"
v-model:value="form.competentDepartment"
/>
</a-form-item>
<a-form-item label="合同其他说明" name="contract">
<a-input
allow-clear
placeholder="请输入合同其他说明"
v-model:value="form.contract"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="项目名称" name="projectName">
<a-input
allow-clear
placeholder="请输入项目名称"
v-model:value="form.projectName"
/>
</a-form-item>
<a-form-item label="租赁单位" name="companyName">
<TowerCompany
:placeholder="`请选择租赁单位`"
v-model:value="form.companyName"
:customer-type="`产权单位`"
@done="chooseCompanyName"
/>
</a-form-item>
<a-form-item label="项目地址" name="projectRegion">
<a-input
allow-clear
placeholder="请输入项目地址"
v-model:value="form.projectRegion"
/>
</a-form-item>
<a-form-item label="经度" name="longitude">
<a-input
allow-clear
placeholder="请输入经度"
v-model:value="form.longitude"
/>
</a-form-item>
<a-form-item label="纬度" name="latitude">
<a-input
allow-clear
placeholder="请输入纬度"
v-model:value="form.latitude"
/>
</a-form-item>
<a-form-item label="施工许可证" name="licenceNo">
<a-input
allow-clear
placeholder="请输入施工许可证"
v-model:value="form.licenceNo"
/>
</a-form-item>
<a-form-item label="安拆单位" name="dismantlingCompany">
<TowerCompany
:placeholder="`请选择安拆单位`"
v-model:value="form.dismantlingCompany"
:customer-type="`产权单位`"
@done="chooseDismantlingCompany"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="项目状态" name="projectStatus">
<DictSelect
dict-code="ProjectStatus"
placeholder="请选择项目状态"
v-model:value="form.projectStatus"
/>
</a-form-item>
<a-form-item label="承租单位" name="customerName">
<TowerCompany
:placeholder="`请选择承租单位`"
v-model:value="form.customerName"
:customer-type="`empty`"
@done="chooseCustomerName"
/>
</a-form-item>
<a-form-item label="详细地址" name="projectAddress">
<a-input
allow-clear
placeholder="请输入详细地址"
v-model:value="form.projectAddress"
/>
</a-form-item>
<a-form-item label="报监编号" name="superviseNo">
<a-input
allow-clear
placeholder="请输入报监编号"
v-model:value="form.superviseNo"
/>
</a-form-item>
<a-form-item label="安全状态" name="securityStatus">
<DictSelect
dict-code="SecurityStatus"
v-model:value="form.securityStatus"
placeholder="选择安全状态"
/>
</a-form-item>
<a-form-item label="汽吊规格" name="truckCraneSpecs">
<a-input
allow-clear
placeholder="请输入汽吊规格"
v-model:value="form.truckCraneSpecs"
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
<a-divider style="height: 8px" />
<a-card title="项目分配" :bordered="false">
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="项目负责人" name="director">
<SelectUser
:placeholder="`请选择所属人员`"
v-model:value="form.director"
@done="chooseDirector"
/>
</a-form-item>
<a-form-item label="项目可见范围" name="title" />
</a-col>
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="项目经理" name="projectDirector">
<a-input
allow-clear
placeholder="请输入项目经理"
v-model:value="form.projectDirector"
/>
</a-form-item>
<a-form-item label="业务负责人" name="salesman">
<SelectUser
:placeholder="`请选择业务负责人`"
v-model:value="form.salesman"
@done="chooseSalesman"
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
<a-divider style="height: 8px" />
<a-card title="项目联系人" :bordered="false">
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
/>
<a-col
v-bind="
styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 16 }
"
/>
<a-col
v-bind="styleResponsive ? { md: 6, sm: 24, xs: 24 } : { span: 8 }"
/>
</a-row>
</a-card>
<a-divider style="height: 8px" />
<a-card title="合同签订设备清单" :bordered="false">
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
/>
<a-col
v-bind="
styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 16 }
"
/>
<a-col
v-bind="styleResponsive ? { md: 6, sm: 24, xs: 24 } : { span: 8 }"
/>
</a-row>
</a-card>
<a-divider style="height: 8px" />
<a-card title="检查/保养部位配置" :bordered="false">
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 8, sm: 24, xs: 24 } : { span: 8 }"
/>
<a-col
v-bind="
styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 16 }
"
/>
<a-col
v-bind="styleResponsive ? { md: 6, sm: 24, xs: 24 } : { span: 8 }"
/>
</a-row>
</a-card>
</a-form>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, computed } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, toDateString } from 'ele-admin-pro';
import { addProject, updateProject } from '@/api/tower/project';
import type { Project } from '@/api/tower/project/model';
import { useUserStore } from '@/store/modules/user';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { UploadOutlined } from '@/layout/menu-icons';
import {
addTowerEquipment,
updateTowerEquipment
} from '@/api/tower/equipment';
import { FormInstance } from 'ant-design-vue/es/form';
import { Customer } from '@/api/oa/customer/model';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import DictSelect from '@/views/search/components/dict-select.vue';
import { User } from '@/api/system/user/model';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Project | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 当期时间
const dayTime = toDateString(new Date());
// 已上传数据
const images = ref<ItemType[]>([]);
// 类型 single|multiple
// const type = ref<string>('single');
// 日期范围选择
const dateRange = ref<[string, string]>(['', '']);
// 穿梭框数据
const breakfast = ref<any[]>();
const targetKeys1 = ref<any[]>([]);
const selectedKeys1 = ref<any[]>();
const lunch = ref<any[]>();
const targetKeys2 = ref<any[]>([]);
const selectedKeys2 = ref<any[]>();
const dinner = ref<any[]>();
const targetKeys3 = ref<any[]>([]);
const selectedKeys3 = ref<any[]>();
const selectedGoodsIds = ref<any[]>([]);
const formRef = ref<FormInstance | null>(null);
// 合并选中的项目
const onChange1 = () => {
selectedGoodsIds.value = [
...targetKeys1.value,
...targetKeys2.value,
...targetKeys3.value
];
};
const onChange2 = () => {
selectedGoodsIds.value = [
...targetKeys1.value,
...targetKeys2.value,
...targetKeys3.value
];
};
const onChange3 = () => {
selectedGoodsIds.value = [
...targetKeys1.value,
...targetKeys2.value,
...targetKeys3.value
];
};
// 用户信息
const form = reactive<Project>({
projectId: undefined,
projectName: '',
companyName: '',
customerName: '',
projectRegion: '',
projectAddress: '',
longitude: '',
latitude: '',
superviseNo: '',
licenceNo: '',
securityStatus: undefined,
competentDepartment: '',
dismantlingCompany: '',
truckCraneSpecs: '',
contract: '',
director: '',
projectDirector: '',
projectStatus: '',
salesman: '',
status: 0,
userId: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
projectName: [
{
required: true,
type: 'string',
message: '请输入项目名称',
trigger: 'blur'
}
],
projectCode: [
{
required: true,
type: 'string',
message: '请输入合法的IP地址',
trigger: 'blur'
}
],
progress: [
{
required: true,
type: 'string',
message: '请选择跟进状态',
trigger: 'blur'
}
],
securityStatus: [
{
required: true,
type: 'string',
message: '请选择安全状态',
trigger: 'blur'
}
],
companyName: [
{
required: true,
type: 'string',
message: '请选择租赁单位',
trigger: 'blur'
}
],
customerName: [
{
required: true,
type: 'string',
message: '请选择承租单位',
trigger: 'blur'
}
],
projectRegion: [
{
required: true,
type: 'string',
message: '请输入项目地址',
trigger: 'blur'
}
]
});
const { resetFields, validate, validateInfos } = useForm(form, rules);
const chooseCompanyName = (data: Customer) => {
form.companyName = data.customerName;
};
const chooseCustomerName = (data: Customer) => {
form.customerName = data.customerName;
};
const chooseDismantlingCompany = (data: Customer) => {
form.dismantlingCompany = data.customerName;
};
const chooseDirector = (data: User) => {
form.director = data.nickname;
};
const chooseSalesman = (data: User) => {
form.salesman = data.nickname;
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateProject : addProject;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
const reload = () => {
loading.value = true;
};
reload();
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignObject(form, props.data);
reload();
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>
<style lang="less"></style>
<style lang="less">
.tab-pane {
min-height: 300px;
}
.ml-10 {
margin-left: 5px;
}
.upload-text {
margin-right: 70px;
}
.upload-image {
margin-bottom: 30px;
display: flex;
justify-content: center;
}
</style>
@@ -0,0 +1,104 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<span>添加项目</span>
</a-button>
<a-button
danger
type="primary"
class="ele-btn-icon"
:disabled="selection.length === 0"
@click="removeBatch"
>
<template #icon>
<DeleteOutlined />
</template>
<span>批量删除</span>
</a-button>
<a-button class="ele-btn-icon">
<span>导入项目</span>
</a-button>
<a-button class="ele-btn-icon">
<span>导出月度设备动态</span>
</a-button>
<a-button class="ele-btn-icon">
<span>导出项目库、设备安装情况</span>
</a-button>
<a-button class="ele-btn-icon">
<span>导出全部项目库存、全部设备安装情况</span>
</a-button>
<a-input-search
allow-clear
placeholder="请输入关键词"
v-model:value="searchText"
@pressEnter="search"
@search="search"
/>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import useSearch from '@/utils/use-search';
import { ref, watch } from 'vue';
import { OrderParam } from '@/api/order/model';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: OrderParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'advanced'): void;
}>();
// 表单数据
const { where, resetFields } = useSearch<OrderParam>({
orderNo: undefined,
userId: undefined,
payStatus: undefined,
deliveryStatus: undefined,
orderStatus: undefined
});
// 下来选项
const type = ref('keywords');
// 搜索内容
const searchText = ref('');
// 日期范围选择
const dateRange = ref<[string, string]>(['', '']);
/* 搜索 */
const search = () => {
const [d1, d2] = dateRange.value ?? [];
if (type.value == 'keywords') {
where.keywords = searchText.value;
}
emit('search', {
...where,
createTimeStart: d1 ? d1 + ' 00:00:00' : '',
createTimeEnd: d2 ? d2 + ' 23:59:59' : ''
});
};
// 发布项目
const add = () => {
emit('add');
};
// 批量删除
const removeBatch = () => {
emit('remove');
};
watch(
() => props.selection,
() => {}
);
</script>
+288
View File
@@ -0,0 +1,288 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="projectId"
:columns="columns"
:datasource="datasource"
v-model:selection="selection"
:customRow="customRow"
tool-class="ele-toolbar-form"
:scroll="{ x: 1200 }"
class="sys-org-table"
:striped="true"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@advanced="openAdvanced"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'nickname'">
{{ record.nickname }}
</template>
<template v-if="column.key === 'createTime'">
{{ record.createTime }}
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">库存查看</a>
<a-divider type="vertical" />
<a @click="openEdit(record)">项目年报</a>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
</div>
<!-- 编辑弹窗 -->
<ProjectEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 订单详情 -->
<order-info v-model:visible="showInfo" :data="current" @done="reload" />
</div>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import ProjectEdit from './components/project-edit.vue';
import OrderInfo from './components/order-info.vue';
import {
pageProject,
removeBatchProject,
removeProject
} from '@/api/tower/project';
import { Project, ProjectParam } from '@/api/tower/project/model';
defineProps<{
activeKey?: boolean;
data?: any;
}>();
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '项目状态',
dataIndex: 'projectStatus',
key: 'projectStatus'
},
{
title: '项目名称',
dataIndex: 'projectName',
key: 'projectName'
},
{
title: '项目地址',
dataIndex: 'projectAddress',
key: 'projectAddress'
},
{
title: '承租单位',
dataIndex: 'customerName',
key: 'customerName'
},
{
title: '项目负责人',
dataIndex: 'director',
key: 'director'
},
{
title: '是否关联',
dataIndex: 'companyId',
key: 'companyId'
},
{
title: '检查频率',
dataIndex: 'companyId',
key: 'companyId'
},
{
title: '保养频率',
dataIndex: 'companyId',
key: 'companyId'
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center',
hideInSetting: true
}
]);
// 表格选中数据
const selection = ref<Project[]>([]);
// 当前编辑数据
const current = ref<Project | null>(null);
// 是否显示资产详情
const showInfo = ref(false);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示高级搜索
const showAdvancedSearch = ref(false);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
// 搜索条件
if (filters.payMethod) {
where.payMethod = filters.payMethod;
}
if (filters.deliveryType) {
where.deliveryType = filters.deliveryType;
}
if (filters.payStatus) {
where.payStatus = filters.payStatus;
}
if (filters.orderSource) {
where.orderSource = filters.orderSource;
}
where.tenantId = localStorage.getItem('tenantId');
where.payStatus = 20;
return pageProject({
...where,
...orders,
page,
limit
});
};
/* 搜索 */
const reload = (where?: ProjectParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: Project) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开用户详情弹窗 */
const openInfo = (row?: Project) => {
current.value = row ?? null;
showInfo.value = true;
};
/* 打开高级搜索 */
const openAdvanced = () => {
showAdvancedSearch.value = !showAdvancedSearch.value;
};
/* 删除单个 */
const remove = (row: Project) => {
const hide = message.loading('请求中..', 0);
removeProject(row.projectId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
console.log(selection.value);
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchProject(selection.value.map((d) => d.projectId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 自定义行属性 */
const customRow = (record: Project) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
reload();
</script>
<script lang="ts">
export default {
name: 'ProjectIndex'
};
</script>
<style lang="less" scoped>
p {
line-height: 0.8;
}
.sys-org-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.sys-org-table :deep(.ant-table-pagination.ant-pagination) {
padding: 0 4px;
margin-bottom: 0;
}
.price-edit {
padding-right: 5px;
}
.comments {
max-width: 200px;
}
</style>
@@ -0,0 +1,339 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
:width="880"
:visible="visible"
:confirm-loading="loading"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑企业' : '添加企业'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
:label-col="{ md: { span: 7 }, sm: { span: 20 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 24 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<a-row :gutter="16">
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="企业名称" v-bind="validateInfos.customerName">
<a-input
allow-clear
:maxlength="30"
placeholder="请输入企业名称"
v-model:value="form.customerName"
@blur="
validate('customerName', { trigger: 'blur' }).catch(() => {})
"
/>
</a-form-item>
<a-form-item label="企业类型" v-bind="validateInfos.customerType">
<DictSelect
dict-code="CustomerType"
placeholder="请选择企业类型"
v-model:value="form.customerType"
/>
</a-form-item>
<a-form-item label="统一信用代码" v-bind="validateInfos.customerCode">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入社会统一信用代码"
v-model:value="form.customerCode"
/>
</a-form-item>
<a-form-item label="企业负责人" v-bind="validateInfos.customerContacts">
<a-input
allow-clear
:maxlength="20"
placeholder="请填写联系人"
v-model:value="form.customerContacts"
/>
</a-form-item>
<a-form-item label="手机号码" v-bind="validateInfos.customerMobile">
<a-input
allow-clear
:maxlength="20"
placeholder="请填写联系人手机号码"
v-model:value="form.customerMobile"
/>
</a-form-item>
<a-form-item label="企业logo" v-bind="validateInfos.customerAvatar">
<ele-image-upload
v-model:value="images"
:item-style="{ width: '90px', height: '90px' }"
:limit="1"
@upload="onUpload"
/>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="企业全称" v-bind="validateInfos.customerFullName">
<a-input
allow-clear
:maxlength="30"
placeholder="请输入企业全称"
v-model:value="form.customerFullName"
@blur="
validate('customerFullName', { trigger: 'blur' }).catch(() => {})
"
/>
</a-form-item>
<a-form-item label="公司座机" v-bind="validateInfos.customerPhone">
<a-input
allow-clear
:maxlength="20"
placeholder="请填写公司座机电话"
v-model:value="form.customerPhone"
/>
</a-form-item>
<a-form-item label="选择位置" name="customerAddress">
<a-input-group compact>
<a-input
disabled
style="width: calc(100% - 32px)"
v-model:value="form.customerAddress"
placeholder="所属区域"
@search="onSearch"
/>
<a-tooltip title="选择位置">
<a-button @click="openMapPicker">
<template #icon><EnvironmentOutlined /></template>
</a-button>
</a-tooltip>
</a-input-group>
</a-form-item>
<a-form-item label="所属区域" name="region">
<regions-select
v-model:value="city"
valueField="label"
placeholder="请选择省市区"
class="ele-fluid"
/>
</a-form-item>
<a-form-item
label="详细地址"
v-bind="validateInfos.customerAddress"
>
<a-input
allow-clear
placeholder="请填写联系地址"
v-model:value="form.customerAddress"
/>
</a-form-item>
<a-form-item label="排序" v-bind="validateInfos.sortNumber">
<a-input
allow-clear
:maxlength="20"
placeholder="排序"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="备注" v-bind="validateInfos.comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.comments"
/>
</a-form-item>
</a-col>
</a-row>
<!-- 地图位置选择弹窗 -->
<ele-map-picker
:need-city="true"
:dark-mode="darkMode"
v-model:visible="showMap"
:center="[108.374959, 22.767024]"
:search-type="1"
:zoom="12"
@done="onDone"
/>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import {ref, reactive, watch, computed} from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import { addCustomer, updateCustomer } from '@/api/oa/customer';
import type { Customer } from '@/api/oa/customer/model';
import { createCode } from '@/utils/common';
import { uploadFile } from '@/api/system/file';
import type { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FILE_SERVER } from '@/config/setting';
import { useUserStore } from '@/store/modules/user';
import DictSelect from "@/views/search/components/dict-select.vue";
import {
EnvironmentOutlined
} from '@ant-design/icons-vue';
import { useThemeStore } from "@/store/modules/theme";
import { storeToRefs } from "pinia";
import { CenterPoint } from "ele-admin-pro/es/ele-map-picker/types";
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Customer | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 省市区
const city = ref<string[]>([]);
// 是否显示地图选择弹窗
const showMap = ref(false);
const themeStore = useThemeStore();
const { darkMode } = storeToRefs(themeStore);
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 用户信息
const form = reactive<Customer>({
customerCode: '',
customerName: '',
customerFullName: '',
customerType: undefined,
progress: undefined,
customerMobile: '',
customerAvatar: '',
customerPhone: '',
customerSource: '',
customerContacts: '',
customerAddress: '',
customerProvince: '',
customerCity: '',
customerRegion: '',
comments: '',
status: '0',
sortNumber: 100,
customerId: 0,
userId: '',
});
// 已上传数据, 可赋初始值用于回显
const images = ref(<any>[]);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
customerName: [
{
required: true,
type: 'string',
message: '请输入企业名称',
trigger: 'blur'
}
],
customerType: [
{
required: true,
type: 'string',
message: '请选择企业类型',
trigger: 'blur'
}
],
});
const { resetFields, validate, validateInfos } = useForm(form, rules);
/* 打开位置选择 */
const openMapPicker = () => {
showMap.value = true;
};
/* 地图选择后回调 */
const onDone = (location: CenterPoint) => {
console.log(location);
city.value = [
`${location.city?.province}`,
`${location.city?.city}`,
`${location.city?.district}`
];
form.customerProvince = `${location.city?.province}`;
form.customerCity = `${location.city?.city}`;
form.customerRegion = `${location.city?.district}`;
form.customerAddress = `${location.address}`;
form.latitude = `${location.lat}`;
form.longitude = `${location.lng}`;
showMap.value = false;
};
/* 保存编辑 */
const save = () => {
validate()
.then(() => {
loading.value = true;
// 去除空格
form.customerName = form.customerName?.replace(/\s*/g, '');
const data = {
...form
};
// 转字符串
const saveOrUpdate = isUpdate.value ? updateCustomer : addCustomer;
saveOrUpdate(data)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
// 上传文件
const onUpload = (d: ItemType) => {
uploadFile(<File>d.file)
.then((result) => {
form.customerAvatar = result.path;
message.success('上传成功');
})
.catch((e) => {
message.error(e.message);
});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
loading.value = false;
// 头像赋值
images.value = [];
if(props.data.customerAvatar){
images.value.push({ uid:1, url: FILE_SERVER + props.data.customerAvatar, status: '' });
}
assignObject(form, props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>
<style lang="less"></style>
@@ -0,0 +1,148 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
width="75%"
:visible="visible"
:confirm-loading="loading"
:title="'企业详情'"
:maxable="true"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
:footer="null"
>
<a-form
:label-col="{ md: { span: 4 }, sm: { span: 24 } }"
:wrapper-col="{ md: { span: 19 }, sm: { span: 24 } }"
>
<div class="base-form" style="margin-bottom: 20px">
<a-descriptions bordered>
<a-descriptions-item label="企业名称">
{{ customer.customerName }}
</a-descriptions-item>
<a-descriptions-item label="社会统一信用代码">
{{ customer.customerCode }}
</a-descriptions-item>
<a-descriptions-item label="跟进状态">
<div color="blue" v-for="(d, index) in progress" :key="index">
<span v-if="d.value == customer.progress">{{ d.label }}</span>
</div>
</a-descriptions-item>
<a-descriptions-item label="联系人">
{{ customer.customerContacts }}
</a-descriptions-item>
<a-descriptions-item label="联系电话">
{{ customer.customerMobile }}
</a-descriptions-item>
<a-descriptions-item label="座机电话">
{{ customer.customerPhone }}
</a-descriptions-item>
<a-descriptions-item label="企业类型">
<div color="blue" v-for="(d, index) in customerType" :key="index">
<span v-if="d.value == customer.customerType">{{ d.value }}</span>
</div>
</a-descriptions-item>
<a-descriptions-item label="联系地址">
{{ customer.customerAddress }}
</a-descriptions-item>
<a-descriptions-item label="备注">
{{ customer.comments }}
</a-descriptions-item>
</a-descriptions>
<!-- <a-descriptions-->
<!-- title="其他信息"-->
<!-- :column="1"-->
<!-- bordered-->
<!-- style="margin-top: 30px"-->
<!-- >-->
<!-- <a-descriptions-item label="相关项目">-->
<!-- {{ customer.comments }}-->
<!-- </a-descriptions-item>-->
<!-- </a-descriptions>-->
</div>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import type { Customer } from '@/api/oa/customer/model';
import { FILE_SERVER } from '@/config/setting';
import { getDictionaryOptions } from '@/utils/common';
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Customer | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 用户信息
const customer = reactive<Customer>({
customerCode: '',
customerName: '',
customerType: undefined,
customerMobile: '',
customerAvatar: '',
customerPhone: '',
customerContacts: '',
customerAddress: '',
comments: '',
progress: '',
status: '0',
sortNumber: 100,
customerId: 0
});
// 请求状态
const loading = ref(true);
const { resetFields } = useForm(customer);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
/* 打开外部链接 */
// const openUrl = (record) => {
// window.open(record.panel);
// };
/* 获取字典数据 */
const customerType = getDictionaryOptions('customerType');
const progress = getDictionaryOptions('customerFollowStatus');
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
loading.value = false;
assignObject(customer, props.data);
}
} else {
resetFields();
}
}
);
</script>
<style lang="less">
.tab-pane {
min-height: 100px;
}
.card-head {
display: flex;
height: 40px;
align-items: center;
margin-bottom: 30px;
}
</style>
@@ -0,0 +1,223 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加企业</span>
</a-button>
<a-upload
:before-upload="importBatch"
:show-upload-list="false"
accept=".xls,.xlsx"
>
<a-button class="ele-btn-icon"> <DownloadOutlined />批量导入 </a-button>
</a-upload>
<a-button class="ele-btn-icon">
<span>下载企业模板</span>
</a-button>
<a-upload
:before-upload="importBatch"
:show-upload-list="false"
accept=".xls,.xlsx"
>
<a-button class="ele-btn-icon"> <UploadOutlined />导出 </a-button>
</a-upload>
<a-button
danger
type="primary"
class="ele-btn-icon"
v-if="selection.length > 0"
@click="removeBatch"
>
<template #icon>
<DeleteOutlined />
</template>
<span>批量删除</span>
</a-button>
<!-- <a-button @click="batchMove" v-if="selection.length > 0">-->
<!-- <template #icon>-->
<!-- <UserSwitchOutlined />-->
<!-- </template>-->
<!-- 批量转移-->
<!-- </a-button>-->
<a-input-search
allow-clear
placeholder="请输入关键词"
v-model:value="searchText"
@pressEnter="search"
@search="search"
/>
</a-space>
</template>
<script lang="ts" setup>
import {
PlusOutlined,
DeleteOutlined,
UploadOutlined,
DownloadOutlined
} from '@ant-design/icons-vue';
import useSearch from '@/utils/use-search';
import type { CustomerParam } from '@/api/oa/customer/model';
import { ref, watch } from 'vue';
import { utils, read } from 'xlsx';
// import { assignObject } from 'ele-admin-pro';
import { message } from 'ant-design-vue/es';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: CustomerParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 表单数据
const { where } = useSearch<CustomerParam>({
customerName: '',
customerCode: '',
nickname: '',
keywords: '',
userId: undefined
});
// 下来选项
// 搜索内容
const searchText = ref('');
/* 搜索 */
const search = () => {
where.keywords = searchText.value;
emit('search', where);
};
// 新增
const add = () => {
emit('add');
};
// 导入数据的列
const importTitle = ref<string[]>(['A', 'B', 'C', 'D', 'E', 'F', 'G']);
// 导入的数据
const importData = ref<Record<string, any>[]>([]);
// 导入数据二维数组形式
const importDataAoa = ref<(string | number)[][]>([]);
/* 导入本地 excel 文件 */
const importBatch = (file: File) => {
if (
![
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
].includes(file.type)
) {
message.error('只能选择 excel 文件');
return false;
}
if (file.size / 1024 / 1024 > 20) {
message.error('大小不能超过 20MB');
return false;
}
const reader = new FileReader();
reader.onload = (e) => {
const data = new Uint8Array(e.target?.result as any);
const workbook = read(data, { type: 'array' });
const sheetNames = workbook.SheetNames;
const worksheet = workbook.Sheets[sheetNames[0]];
// 解析成二维数组
const aoa = utils.sheet_to_json<string[]>(worksheet, { header: 1 });
// 生成表格需要的数据
let list: Record<string, any>[] = [];
let maxCols = 0;
let title: string[] = [];
aoa.forEach((d) => {
if (d.length > maxCols) {
maxCols = d.length;
}
const row = {};
for (let i = 0; i < d.length; i++) {
const key = getCharByIndex(i);
row[key] = d[i];
row['__colspan__' + key] = 1;
row['__rowspan__' + key] = 1;
}
list.push(row);
});
for (let i = 0; i < maxCols; i++) {
title.push(getCharByIndex(i));
}
importTitle.value = title;
importData.value = list;
importDataAoa.value = aoa;
};
console.log(importData.value);
console.log(importDataAoa.value);
reader.readAsArrayBuffer(file);
return false;
};
/* 生成Excel列字母序号 */
const getCharByIndex = (index: number) => {
const chars = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z'
];
if (index < chars.length) {
return chars[index];
}
const n = parseInt(String(index / chars.length));
const m = index % chars.length;
return chars[n] + chars[m];
};
// 转移
// const batchMove = () => {
// emit('batchMove');
// };
// 批量删除
const removeBatch = () => {
emit('remove');
};
const onClear = () => {
where.userId = undefined;
search();
};
watch(
() => props.selection,
() => {}
);
</script>
+309
View File
@@ -0,0 +1,309 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="customerId"
:columns="columns"
:datasource="datasource"
v-model:selection="selection"
:customRow="customRow"
tool-class="ele-toolbar-form"
:scroll="{ x: 800 }"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'customerName'">
<a-avatar
:size="30"
:src="`${FILE_THUMBNAIL + record.customerAvatar}`"
style="margin-right: 4px"
:srcset="`${FILE_THUMBNAIL + record.customerAvatar}`"
>
<template #icon>
<UserOutlined />
</template>
</a-avatar>
<a-tooltip title="查看详情">
<a href="#" @click="openEdit(record)">{{
record.customerName
}}</a>
</a-tooltip>
</template>
<template v-if="column.key === 'customerType'">
<div v-for="(d, i) in JSON.parse(customerType)" :key="i">
<span v-if="d.value === record.customerType">{{
d.value
}}</span>
</div>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">正常</a-tag>
<a-tag v-if="record.status === 1" color="red">待审核</a-tag>
<a-tag v-if="record.status === 2" color="purple">已驳回</a-tag>
</template>
<template v-if="column.key === 'nickname'">
<a-tooltip :title="`${record.nickname}`">
<a-avatar :src="record.userAvatar" size="small" />
</a-tooltip>
</template>
<template v-if="column.key === 'createTime'">
<a-tooltip :title="`${toDateString(record.createTime)}`">
{{ timeAgo(record.createTime) }}
</a-tooltip>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<CustomerEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 企业详情弹窗 -->
<CustomerInfo v-model:visible="showInfo" :data="current" @done="reload" />
<!-- 批量转移弹窗 -->
<!-- <CustomerMove v-model:visible="showMove" :data="selection" @done="batchMove" />-->
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import {
ExclamationCircleOutlined,
UserOutlined
} from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { toDateString } from 'ele-admin-pro';
import Search from './components/search.vue';
import CustomerEdit from './components/customer-edit.vue';
import CustomerInfo from './components/customer-info.vue';
import {
pageCustomer,
removeCustomer,
removeBatchCustomer
} from '@/api/oa/customer';
import { timeAgo } from 'ele-admin-pro';
import type { Customer, CustomerParam } from '@/api/oa/customer/model';
import { useUserStore } from '@/store/modules/user';
import { FILE_THUMBNAIL } from '@/config/setting';
const userStore = useUserStore();
// 当前用户信息
const loginUser = computed(() => userStore.info ?? {});
const customerType = localStorage.getItem('customerType');
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<Customer[]>([]);
// 当前编辑数据
const current = ref<Customer | null>(null);
// 是否显示资产详情
const showInfo = ref(false);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.progress = filters.progress;
where.customerSource = filters.customerSource;
where.customerType = filters.customerType;
where.status = filters.status;
}
return pageCustomer({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '企业名称',
dataIndex: 'customerName',
key: 'customerName'
},
{
title: '企业类型',
dataIndex: 'customerType',
key: 'customerType'
},
{
title: '企业负责人',
dataIndex: 'customerContacts'
},
{
title: '联系电话',
dataIndex: 'customerPhone'
},
{
title: '是否含税',
dataIndex: 'customerMobile'
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: CustomerParam) => {
console.log(where);
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: Customer) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 打开用户详情弹窗 */
const openInfo = (row?: Customer) => {
current.value = row ?? null;
showInfo.value = true;
};
/* 删除单个 */
const remove = (row: Customer) => {
const hide = message.loading('请求中..', 0);
removeCustomer(row.customerId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量转移 */
const batchMove = (userId) => {
console.log(userId, '批量转移0000');
console.log(selection.value);
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchCustomer(
selection.value.map((d) => {
if (loginUser.value.userId === d.userId) {
return d.customerId;
}
})
)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 自定义行属性 */
const customRow = (record: Customer) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
</script>
<script lang="ts">
export default {
name: 'Accessory'
};
</script>
<style lang="less" scoped>
.sys-org-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.sys-org-table :deep(.ant-table-pagination.ant-pagination) {
padding: 0 4px;
margin-bottom: 0;
}
</style>
@@ -0,0 +1,223 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加仓库</span>
</a-button>
<a-upload
:before-upload="importBatch"
:show-upload-list="false"
accept=".xls,.xlsx"
>
<a-button class="ele-btn-icon"> <DownloadOutlined />批量导入 </a-button>
</a-upload>
<a-button class="ele-btn-icon">
<span>下载仓库模板</span>
</a-button>
<a-upload
:before-upload="importBatch"
:show-upload-list="false"
accept=".xls,.xlsx"
>
<a-button class="ele-btn-icon"> <UploadOutlined />导出 </a-button>
</a-upload>
<a-button
danger
type="primary"
class="ele-btn-icon"
v-if="selection.length > 0"
@click="removeBatch"
>
<template #icon>
<DeleteOutlined />
</template>
<span>批量删除</span>
</a-button>
<!-- <a-button @click="batchMove" v-if="selection.length > 0">-->
<!-- <template #icon>-->
<!-- <UserSwitchOutlined />-->
<!-- </template>-->
<!-- 批量转移-->
<!-- </a-button>-->
<a-input-search
allow-clear
placeholder="请输入关键词"
v-model:value="searchText"
@pressEnter="search"
@search="search"
/>
</a-space>
</template>
<script lang="ts" setup>
import {
PlusOutlined,
DeleteOutlined,
UploadOutlined,
DownloadOutlined
} from '@ant-design/icons-vue';
import useSearch from '@/utils/use-search';
import type { CustomerParam } from '@/api/oa/customer/model';
import { ref, watch } from 'vue';
import { utils, read } from 'xlsx';
// import { assignObject } from 'ele-admin-pro';
import { message } from 'ant-design-vue/es';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: CustomerParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 表单数据
const { where } = useSearch<CustomerParam>({
customerName: '',
customerCode: '',
nickname: '',
keywords: '',
userId: undefined
});
// 下来选项
// 搜索内容
const searchText = ref('');
/* 搜索 */
const search = () => {
where.keywords = searchText.value;
emit('search', where);
};
// 新增
const add = () => {
emit('add');
};
// 导入数据的列
const importTitle = ref<string[]>(['A', 'B', 'C', 'D', 'E', 'F', 'G']);
// 导入的数据
const importData = ref<Record<string, any>[]>([]);
// 导入数据二维数组形式
const importDataAoa = ref<(string | number)[][]>([]);
/* 导入本地 excel 文件 */
const importBatch = (file: File) => {
if (
![
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
].includes(file.type)
) {
message.error('只能选择 excel 文件');
return false;
}
if (file.size / 1024 / 1024 > 20) {
message.error('大小不能超过 20MB');
return false;
}
const reader = new FileReader();
reader.onload = (e) => {
const data = new Uint8Array(e.target?.result as any);
const workbook = read(data, { type: 'array' });
const sheetNames = workbook.SheetNames;
const worksheet = workbook.Sheets[sheetNames[0]];
// 解析成二维数组
const aoa = utils.sheet_to_json<string[]>(worksheet, { header: 1 });
// 生成表格需要的数据
let list: Record<string, any>[] = [];
let maxCols = 0;
let title: string[] = [];
aoa.forEach((d) => {
if (d.length > maxCols) {
maxCols = d.length;
}
const row = {};
for (let i = 0; i < d.length; i++) {
const key = getCharByIndex(i);
row[key] = d[i];
row['__colspan__' + key] = 1;
row['__rowspan__' + key] = 1;
}
list.push(row);
});
for (let i = 0; i < maxCols; i++) {
title.push(getCharByIndex(i));
}
importTitle.value = title;
importData.value = list;
importDataAoa.value = aoa;
};
console.log(importData.value);
console.log(importDataAoa.value);
reader.readAsArrayBuffer(file);
return false;
};
/* 生成Excel列字母序号 */
const getCharByIndex = (index: number) => {
const chars = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z'
];
if (index < chars.length) {
return chars[index];
}
const n = parseInt(String(index / chars.length));
const m = index % chars.length;
return chars[n] + chars[m];
};
// 转移
// const batchMove = () => {
// emit('batchMove');
// };
// 批量删除
const removeBatch = () => {
emit('remove');
};
const onClear = () => {
where.userId = undefined;
search();
};
watch(
() => props.selection,
() => {}
);
</script>
@@ -0,0 +1,330 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
:width="750"
:visible="visible"
:confirm-loading="loading"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑仓库' : '添加仓库'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
:label-col="{ md: { span: 6 }, sm: { span: 20 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 24 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<a-row :gutter="16">
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="仓库名称" name="warehouseName">
<a-input
allow-clear
:maxlength="30"
placeholder="请输入仓库名称"
v-model:value="form.warehouseName"
/>
</a-form-item>
<a-form-item label="负责人" name="warehouseKeeper">
<a-input
allow-clear
:maxlength="30"
placeholder="请输入负责人"
v-model:value="form.warehouseKeeper"
/>
</a-form-item>
<a-form-item label="负责人电话" name="warehousePhone">
<a-input
allow-clear
:maxlength="30"
placeholder="请输入负责人联系电话"
v-model:value="form.warehousePhone"
/>
</a-form-item>
<a-form-item label="排序" name="sortNumber">
<a-input
allow-clear
:maxlength="20"
placeholder="排序"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="备注" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.comments"
/>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="所属区域" name="region">
<a-input-group compact>
<a-input
disabled
style="width: calc(100% - 32px)"
v-model:value="form.region"
placeholder="所属区域"
@search="onSearch"
/>
<a-tooltip title="选择位置">
<a-button @click="openMapPicker">
<template #icon><EnvironmentOutlined /></template>
</a-button>
</a-tooltip>
</a-input-group>
</a-form-item>
<a-form-item label="区域名称" name="regionName">
<a-input
allow-clear
:maxlength="30"
placeholder="请输入区域名称"
v-model:value="form.regionName"
/>
</a-form-item>
<a-form-item
label="联系地址"
name="address"
>
<a-input
allow-clear
placeholder="请填写联系地址"
v-model:value="form.address"
/>
</a-form-item>
<a-form-item label="经度" name="longitude">
<a-input
allow-clear
:maxlength="30"
disabled
placeholder="请输入经度"
v-model:value="form.longitude"
/>
</a-form-item>
<a-form-item label="纬度" name="latitude">
<a-input
allow-clear
:maxlength="30"
disabled
placeholder="请输入纬度"
v-model:value="form.latitude"
/>
</a-form-item>
</a-col>
</a-row>
<!-- 地图位置选择弹窗 -->
<ele-map-picker
:need-city="true"
:dark-mode="darkMode"
v-model:visible="showMap"
:center="[108.374959, 22.767024]"
:search-type="1"
:zoom="12"
@done="onDone"
/>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import {ref, reactive, watch, computed} from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import { addWarehouse, updateWarehouse } from '@/api/tower/warehouse';
import type { Warehouse } from '@/api/tower/warehouse/model';
import { uploadFile } from '@/api/system/file';
import type { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FILE_SERVER } from '@/config/setting';
import { useUserStore } from '@/store/modules/user';
import {
EnvironmentOutlined
} from '@ant-design/icons-vue';
import { CenterPoint } from "ele-admin-pro/es/ele-map-picker/types";
import { useThemeStore } from "@/store/modules/theme";
import { storeToRefs } from "pinia";
const userStore = useUserStore();
// 当前用户信息
const loginUser = computed(() => userStore.info ?? {});
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
const themeStore = useThemeStore();
const { darkMode } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Warehouse | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 用户信息
const form = reactive<Warehouse>({
warehouseName: '',
warehouseKeeper: undefined,
warehousePhone: '',
regionName: '',
longitude: '',
latitude: '',
country: '',
province: '',
city: '',
region: '',
address: '',
status: 0,
userId: undefined,
comments: '',
sortNumber: 100,
warehouseId: 0,
});
// 已上传数据, 可赋初始值用于回显
const images = ref(<any>[]);
// 省市区
const city = ref<string[]>([]);
// 是否显示地图选择弹窗
const showMap = ref(false);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
/* 打开位置选择 */
const openMapPicker = () => {
showMap.value = true;
};
// 选择地区
const onChangeRegion = (value) => {
form.province = value[0];
form.city = value[1];
form.region = value[2];
};
/* 地图选择后回调 */
const onDone = (location: CenterPoint) => {
console.log(location);
city.value = [
`${location.city?.province}`,
`${location.city?.city}`,
`${location.city?.district}`
];
form.province = `${location.city?.province}`;
form.city = `${location.city?.city}`;
form.region = `${location.city?.district}`;
form.regionName = `${location.city?.city}${location.city?.district}`;
form.address = `${location.address}`;
form.latitude = `${location.lat}`;
form.longitude = `${location.lng}`;
showMap.value = false;
};
// 表单验证规则
const rules = reactive({
warehouseName: [
{
required: true,
type: 'string',
message: '请输入仓库名称',
trigger: 'blur'
}
],
warehouseCode: [
{
required: true,
type: 'string',
message: '请输入合法的IP地址',
trigger: 'blur'
}
],
progress: [
{
required: true,
type: 'string',
message: '请选择跟进状态',
trigger: 'blur'
}
]
});
const { resetFields, validate, validateInfos } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
validate()
.then(() => {
loading.value = true;
// 去除空格
form.warehouseName = form.warehouseName?.replace(/\s*/g, '');
// 判断权限
// if (loginUser.value.roles?.[0].roleCode != 'admin'){
// form.status = '1';
// }
const data = {
...form
};
// 转字符串
const saveOrUpdate = isUpdate.value ? updateWarehouse : addWarehouse;
saveOrUpdate(data)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
// 上传文件
const onUpload = (d: ItemType) => {
uploadFile(<File>d.file)
.then((result) => {
form.warehouseAvatar = result.path;
message.success('上传成功');
})
.catch((e) => {
message.error(e.message);
});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
loading.value = false;
// 头像赋值
images.value = [];
if(props.data.warehouseAvatar){
images.value.push({ uid:1, url: FILE_SERVER + props.data.warehouseAvatar, status: '' });
}
assignObject(form, props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>
<style lang="less"></style>
+40
View File
@@ -0,0 +1,40 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<list />
</a-card>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import List from './list.vue';
// 加载状态
const loading = ref(true);
/* 查询 */
const query = () => {
loading.value = true;
};
query();
</script>
<script lang="ts">
export default {
name: 'Customer'
};
</script>
<style lang="less" scoped>
.sys-organization-list {
padding: 12px 6px;
height: calc(100vh - 242px);
border-width: 1px;
border-style: solid;
overflow: auto;
}
</style>
+256
View File
@@ -0,0 +1,256 @@
<template>
<div class="ele-body">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="warehouseId"
:columns="columns"
:datasource="datasource"
v-model:selection="selection"
tool-class="ele-toolbar-form"
:scroll="{ x: 800 }"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'warehouseName'">
<a-tooltip title="查看详情">
<a href="#" @click="openEdit(record)">{{ record.warehouseName }}</a>
</a-tooltip>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">正常</a-tag>
<a-tag v-if="record.status === 1" color="red">待审核</a-tag>
<a-tag v-if="record.status === 2" color="purple">已驳回</a-tag>
</template>
<template v-if="column.key === 'region'">
<a-tooltip
:title="`${record.province} ${record.city} ${record.region}`"
>
<span>{{ `${record.city}${record.region}` }}</span>
</a-tooltip>
</template>
<template v-if="column.key === 'createTime'">
<a-tooltip :title="`${toDateString(record.createTime)}`">
{{ timeAgo(record.createTime) }}
</a-tooltip>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
<!-- 编辑弹窗 -->
<WarehouseEdit v-model:visible="showEdit" :data="current" @done="reload" />
</div>
</template>
<script lang="ts" setup>
import { computed, createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { toDateString } from 'ele-admin-pro';
import Search from './components/search.vue';
import WarehouseEdit from './components/warehouse-edit.vue';
import {
pageWarehouse,
removeWarehouse,
removeBatchWarehouse
} from '@/api/tower/warehouse';
import { timeAgo } from 'ele-admin-pro';
import type { Warehouse, WarehouseParam } from '@/api/tower/warehouse/model';
import { useUserStore } from '@/store/modules/user';
// 当前用户信息
const userStore = useUserStore();
const loginUser = computed(() => userStore.info ?? {});
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<Warehouse[]>([]);
// 当前编辑数据
const current = ref<Warehouse | null>(null);
// 是否显示资产详情
const showInfo = ref(false);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageWarehouse({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '仓库名称',
dataIndex: 'warehouseName',
key: 'warehouseName',
ellipsis: true
},
{
title: '仓库负责人',
dataIndex: 'warehouseKeeper'
},
{
title: '负责人电话',
dataIndex: 'warehousePhone'
},
{
title: '所在区域',
dataIndex: 'region',
key: 'region'
},
{
title: '仓库地址',
dataIndex: 'address'
},
{
title: '塔吊数',
dataIndex: 'tower'
},
{
title: '升降机数',
dataIndex: 'lift'
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: WarehouseParam) => {
console.log(where);
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: Warehouse) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: Warehouse) => {
const hide = message.loading('请求中..', 0);
removeWarehouse(row.warehouseId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchWarehouse(
selection.value.map((d) => {
if (loginUser.value.userId === d.userId) {
return d.warehouseId;
}
})
)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
</script>
<script lang="ts">
export default {
name: 'TowerWarehouse'
};
</script>
<style lang="less" scoped>
.sys-org-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.sys-org-table :deep(.ant-table-pagination.ant-pagination) {
padding: 0 4px;
margin-bottom: 0;
}
</style>