feat: develop export and domain

This commit is contained in:
louis 2024-04-17 09:59:31 +08:00
parent 783f92bf18
commit 89d1aeedf0
13 changed files with 714 additions and 538 deletions

View File

@ -0,0 +1,26 @@
import { request, type RequestOptions } from '@/utils/request';
/** 获取公司列表 GET /api/domain */
export async function domainList(params: API.DomainParams, options?: RequestOptions) {
return request<{
items?: API.DomainEntity[];
meta?: {
itemCount?: number;
totalItems?: number;
itemsPerPage?: number;
totalPages?: number;
currentPage?: number;
};
}>('/api/domain', {
method: 'GET',
params: {
// page has a default value: 1
page: '1',
// pageSize has a default value: 10
pageSize: '10',
...params,
},
...(options || {}),
});
}

View File

@ -36,9 +36,10 @@ import * as saleQuotationGroup from './saleQuotationGroup';
import * as saleQuotationComponent from './saleQuotationComponent'; import * as saleQuotationComponent from './saleQuotationComponent';
import * as saleQuotationTemplate from './saleQuotationTemplate'; import * as saleQuotationTemplate from './saleQuotationTemplate';
import * as saleQuotation from './saleQuotation'; import * as saleQuotation from './saleQuotation';
import * as domain from './domain';
export default { export default {
auth, auth,
domain,
account, account,
captcha, captcha,
authEmail, authEmail,
@ -71,5 +72,5 @@ export default {
saleQuotationGroup, saleQuotationGroup,
saleQuotationComponent, saleQuotationComponent,
saleQuotationTemplate, saleQuotationTemplate,
saleQuotation saleQuotation,
}; };

View File

@ -1,5 +1,19 @@
import { request, type RequestOptions } from '@/utils/request'; import { request, type RequestOptions } from '@/utils/request';
/** 导出出入库记录 GET /api/materials-inventory/export*/
export async function materialsInoutExport(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.MaterialsInoutExportParams,
options?: RequestOptions,
) {
const { ...queryParams } = params;
return request(`/api/materials-in-out/export`, {
method: 'GET',
params: { ...queryParams },
...(options || { responseType: 'blob', isReturnResult: false }),
});
}
/** 获取原材料出入库记录列表 GET /api/materials-in-out */ /** 获取原材料出入库记录列表 GET /api/materials-in-out */
export async function materialsInOutList( export async function materialsInOutList(
params: API.MaterialsInOutListParams, params: API.MaterialsInOutListParams,
@ -60,7 +74,7 @@ export async function materialsInOutInfo(
/** 解除原材料出入库记录和附件关联 PUT /api/materials-in-out/unlink-attachments/${param0} */ /** 解除原材料出入库记录和附件关联 PUT /api/materials-in-out/unlink-attachments/${param0} */
export async function unlinkAttachments( export async function unlinkAttachments(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象) // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.MaterialsInOutUpdateParams, params: API.MaterialsInOutUpdateParams,
body: API.MaterialsInOutUpdateDto, body: API.MaterialsInOutUpdateDto,
options?: RequestOptions, options?: RequestOptions,
) { ) {

View File

@ -21,6 +21,8 @@ declare namespace API {
remark: string; remark: string;
/** 头像 */ /** 头像 */
avatar: string; avatar: string;
/** 所属域 */
domain: DomainType;
}; };
type AccountMenus = { type AccountMenus = {
@ -1213,12 +1215,13 @@ declare namespace API {
status: number; status: number;
roles: RoleEntity[]; roles: RoleEntity[];
dept: DeptEntity; dept: DeptEntity;
domain: DomainType;
accessTokens: AccessTokenEntity[]; accessTokens: AccessTokenEntity[];
id: number; id: number;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}; };
type DomainType = number;
type UserListParams = { type UserListParams = {
page?: number; page?: number;
pageSize?: number; pageSize?: number;
@ -1379,8 +1382,9 @@ declare namespace API {
}; };
type MaterialsInventoryExportParams = { type MaterialsInventoryExportParams = {
time: string; time: string[];
projectId: number; filename:string;
projectId?: number;
}; };
type MaterialsInventoryInfoParams = { type MaterialsInventoryInfoParams = {
@ -1808,6 +1812,11 @@ declare namespace API {
}; };
// Materials In out history // Materials In out history
type MaterialsInoutExportParams = {
time: string[];
filename:string;
};
type MaterialsInOutUpdateParams = { type MaterialsInOutUpdateParams = {
id: number; id: number;
fileIds?: number[]; fileIds?: number[];
@ -2015,4 +2024,18 @@ declare namespace API {
type SaleQuotationTemplateDeleteParams = { type SaleQuotationTemplateDeleteParams = {
id: number; id: number;
}; };
type DomainEntity = {
title: string;
id: number;
};
type DomainParams = {
page?: number;
pageSize?: number;
title?: string;
field?: string;
order?: 'ASC' | 'DESC';
_t?: number;
};
} }

View File

@ -58,11 +58,11 @@ export const useExportExcelModal = () => {
modalProps: { modalProps: {
title: t('component.excel.exportModalTitle'), title: t('component.excel.exportModalTitle'),
onFinish: async (values) => { onFinish: async (values) => {
const { filename, bookType } = values; const { filename, bookType, time } = values;
onOk({ onOk({
filename: `${filename.split('.').shift()}.${bookType}`, filename: bookType ? `${filename.split('.').shift()}.${bookType}` : filename,
bookType, bookType,
time,
}); });
}, },
}, },

View File

@ -12,7 +12,12 @@ export const USER_INFO_KEY = 'USER__INFO__';
// role info key // role info key
export const ROLES_KEY = 'ROLES__KEY__'; export const ROLES_KEY = 'ROLES__KEY__';
/** 是否锁屏 */ /** 是否锁屏 */
export const IS_LOCKSCREEN = 'IS_LOCKSCREEN'; export const IS_LOCKSCREEN = 'IS_LOCKSCREEN';
/** 标签页 */ /** 标签页 */
export const TABS_ROUTES = 'TABS_ROUTES'; export const TABS_ROUTES = 'TABS_ROUTES';
/** 域 */
export const DOMAIN_KEY = 'DOMAIN__';

View File

@ -0,0 +1,51 @@
<template>
<div>
公司 <a-select ref="select"
v-if="options.length"
:disabled="!$auth('system:domain:change')"
v-model:value="userStore.domain"
style="min-width: 100px;"
placeholder="请选择"
@change="onChange"
:dropdownStyle="{ 'textOverflow': 'ellipsis', 'min-width': 'fit-content' }">
<a-select-option :value="item.value"
v-for="(item, index) in options"
:key="index">{{ item.label }}</a-select-option>
</a-select>
</div>
</template>
<script setup lang='ts'>
import Api from '@/api';
import { useUserStore } from '@/store/modules/user';
import type { SelectValue } from 'ant-design-vue/es/select';
import { onMounted, ref } from 'vue';
defineOptions({
name: 'DomianPicker'
})
const userStore = useUserStore();
const options = ref<{ label: string, value: number }[]>([]);
let domains = ref<API.DomainEntity[]>([]);
const onChange = (value: SelectValue) => {
userStore.changeDomain(value)
};
onMounted(() => {
Api.domain.domainList({}).then((res) => {
if (res) {
domains.value = res.items ?? [];
options.value = domains.value.map((item) => {
return {
label: item.title,
value: item.id
}
})
}
})
})
</script>
<style lang='less' scoped></style>

View File

@ -1,3 +1,4 @@
export { default as Search } from './search/index.vue'; export { default as Search } from './search/index.vue';
export { default as FullScreen } from './fullscreen/index.vue'; export { default as FullScreen } from './fullscreen/index.vue';
export { default as ProjectSetting } from './setting/index.vue'; export { default as ProjectSetting } from './setting/index.vue';
export { default as DomianPicker } from './domianPicker/index.vue';

View File

@ -1,23 +1,26 @@
<template> <template>
<Layout.Header :style="headerStyle" class="layout-header"> <Layout.Header :style="headerStyle"
class="layout-header">
<Space :size="20"> <Space :size="20">
<slot> <slot>
<Space :size="20"> <Space :size="20">
<span class="menu-fold" @click="() => emit('update:collapsed', !collapsed)"> <span class="menu-fold"
@click="() => emit('update:collapsed', !collapsed)">
<component :is="collapsed ? MenuUnfoldOutlined : MenuFoldOutlined" /> <component :is="collapsed ? MenuUnfoldOutlined : MenuFoldOutlined" />
</span> </span>
<Breadcrumb> <Breadcrumb>
<template v-for="(routeItem, rotueIndex) in menus" :key="routeItem?.name"> <template v-for="(routeItem, rotueIndex) in menus"
:key="routeItem?.name">
<Breadcrumb.Item> <Breadcrumb.Item>
<TitleI18n :title="routeItem?.meta?.title" /> <TitleI18n :title="routeItem?.meta?.title" />
<template v-if="routeItem?.children?.length" #overlay> <template v-if="routeItem?.children?.length"
#overlay>
<Menu :selected-keys="getSelectKeys(rotueIndex)"> <Menu :selected-keys="getSelectKeys(rotueIndex)">
<template v-for="childItem in routeItem?.children" :key="childItem.name"> <template v-for="childItem in routeItem?.children"
<Menu.Item :key="childItem.name">
v-if="!childItem.meta?.hideInMenu && !childItem.meta?.hideInBreadcrumb" <Menu.Item v-if="!childItem.meta?.hideInMenu && !childItem.meta?.hideInBreadcrumb"
:key="childItem.name" :key="childItem.name"
@click="clickMenuItem(childItem)" @click="clickMenuItem(childItem)">
>
<TitleI18n :title="childItem.meta?.title" /> <TitleI18n :title="childItem.meta?.title" />
</Menu.Item> </Menu.Item>
</template> </template>
@ -30,14 +33,17 @@
</slot> </slot>
</Space> </Space>
<Space :size="20"> <Space :size="20">
<DomianPicker />
<Search /> <Search />
<Tooltip :title="$t('layout.header.tooltipLock')" placement="bottom"> <Tooltip :title="$t('layout.header.tooltipLock')"
placement="bottom">
<LockOutlined @click="lockscreenStore.setLock(true)" /> <LockOutlined @click="lockscreenStore.setLock(true)" />
</Tooltip> </Tooltip>
<FullScreen /> <FullScreen />
<!-- <LocalePicker /> --> <!-- <LocalePicker /> -->
<Dropdown placement="bottomRight"> <Dropdown placement="bottomRight">
<Avatar :src="userInfo.avatar" :alt="userInfo.username">{{ userInfo.username }}</Avatar> <Avatar :src="userInfo.avatar"
:alt="userInfo.username">{{ userInfo.username }}</Avatar>
<template #overlay> <template #overlay>
<Menu> <Menu>
<Menu.Item @click="$router.push({ name: 'account-about' })"> <Menu.Item @click="$router.push({ name: 'account-about' })">
@ -61,151 +67,151 @@
</template> </template>
<script lang="tsx" setup> <script lang="tsx" setup>
import { computed, type CSSProperties } from 'vue'; import { computed, type CSSProperties } from 'vue';
import { useRouter, useRoute, type RouteRecordRaw } from 'vue-router'; import { useRouter, useRoute, type RouteRecordRaw } from 'vue-router';
import { import {
QuestionCircleOutlined, QuestionCircleOutlined,
MenuFoldOutlined, MenuFoldOutlined,
MenuUnfoldOutlined, MenuUnfoldOutlined,
PoweroffOutlined, PoweroffOutlined,
LockOutlined, LockOutlined,
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { import {
Layout, Layout,
Modal, Modal,
Dropdown, Dropdown,
Menu, Menu,
Space, Space,
Breadcrumb, Breadcrumb,
Avatar, Avatar,
Tooltip, Tooltip,
type MenuTheme, type MenuTheme,
} from 'ant-design-vue'; } from 'ant-design-vue';
import { Search, FullScreen, ProjectSetting } from './components/'; import { Search, FullScreen, ProjectSetting, DomianPicker } from './components/';
import { LocalePicker } from '@/components/basic/locale-picker'; import { LocalePicker } from '@/components/basic/locale-picker';
import { useUserStore } from '@/store/modules/user'; import { useUserStore } from '@/store/modules/user';
import { useLockscreenStore } from '@/store/modules/lockscreen'; import { useLockscreenStore } from '@/store/modules/lockscreen';
import { TitleI18n } from '@/components/basic/title-i18n'; import { TitleI18n } from '@/components/basic/title-i18n';
import { useLayoutSettingStore } from '@/store/modules/layoutSetting'; import { useLayoutSettingStore } from '@/store/modules/layoutSetting';
defineProps({ defineProps({
collapsed: { collapsed: {
type: Boolean, type: Boolean,
}, },
theme: { theme: {
type: String as PropType<MenuTheme>, type: String as PropType<MenuTheme>,
}, },
}); });
const emit = defineEmits(['update:collapsed']); const emit = defineEmits(['update:collapsed']);
const userStore = useUserStore(); const userStore = useUserStore();
const layoutSettingStore = useLayoutSettingStore(); const layoutSettingStore = useLayoutSettingStore();
const lockscreenStore = useLockscreenStore(); const lockscreenStore = useLockscreenStore();
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const userInfo = computed(() => userStore.userInfo); const userInfo = computed(() => userStore.userInfo);
const headerStyle = computed<CSSProperties>(() => { const headerStyle = computed<CSSProperties>(() => {
const { navTheme, layout } = layoutSettingStore.layoutSetting; const { navTheme, layout } = layoutSettingStore.layoutSetting;
const isDark = navTheme === 'dark' && layout === 'topmenu'; const isDark = navTheme === 'dark' && layout === 'topmenu';
return { return {
backgroundColor: navTheme === 'realDark' || isDark ? '' : 'rgba(255, 255, 255, 0.85)', backgroundColor: navTheme === 'realDark' || isDark ? '' : 'rgba(255, 255, 255, 0.85)',
color: isDark ? 'rgba(255, 255, 255, 0.85)' : '', color: isDark ? 'rgba(255, 255, 255, 0.85)' : '',
};
});
const menus = computed(() => {
if (route.meta?.namePath) {
let children = userStore.menus;
const paths = route.meta?.namePath?.map((item) => {
const a = children.find((n) => n.name === item);
children = a?.children || [];
return a;
});
return [
{
name: '__index',
meta: {
title: '首页',
},
children: userStore.menus,
},
...paths,
];
}
return route.matched;
});
const getSelectKeys = (rotueIndex: number) => {
return [menus.value[rotueIndex + 1]?.name] as string[];
}; };
});
const findLastChild = (route?: RouteRecordRaw) => { const menus = computed(() => {
if (typeof route?.redirect === 'object') { if (route.meta?.namePath) {
const redirectValues = Object.values(route.redirect); let children = userStore.menus;
if (route?.children?.length) { const paths = route.meta?.namePath?.map((item) => {
const target = route.children.find((n) => const a = children.find((n) => n.name === item);
redirectValues.some((m) => [n.name, n.path, n.meta?.fullPath].some((v) => v === m)), children = a?.children || [];
); return a;
return findLastChild(target);
}
return redirectValues.find((n) => typeof n === 'string');
} else if (typeof route?.redirect === 'string') {
if (route?.children?.length) {
const target = route.children.find((n) =>
[n.name, n.path, n.meta?.fullPath].some((m) => m === route?.redirect),
);
return findLastChild(target);
}
return route?.redirect;
}
return route;
};
const getRouteByName = (name: string) => router.getRoutes().find((n) => n.name === name);
//
const clickMenuItem = (menuItem: RouteRecordRaw) => {
const lastChild = findLastChild(menuItem);
console.log('lastChild', menuItem, lastChild);
const targetRoute = getRouteByName(lastChild?.name);
const { isExt, extOpenMode } = targetRoute?.meta || {};
if (isExt && extOpenMode === 1) {
window.open(lastChild?.path);
} else {
router.push({ name: lastChild?.name });
}
};
// 退
const doLogout = () => {
Modal.confirm({
title: '您确定要退出登录吗?',
icon: <QuestionCircleOutlined />,
centered: true,
onOk: async () => {
// rootadmin退
if (userStore.userInfo.phone !== '13553550634') {
// logout({})
await userStore.logout();
}
},
}); });
}; return [
{
name: '__index',
meta: {
title: '首页',
},
children: userStore.menus,
},
...paths,
];
}
return route.matched;
});
const getSelectKeys = (rotueIndex: number) => {
return [menus.value[rotueIndex + 1]?.name] as string[];
};
const findLastChild = (route?: RouteRecordRaw) => {
if (typeof route?.redirect === 'object') {
const redirectValues = Object.values(route.redirect);
if (route?.children?.length) {
const target = route.children.find((n) =>
redirectValues.some((m) => [n.name, n.path, n.meta?.fullPath].some((v) => v === m)),
);
return findLastChild(target);
}
return redirectValues.find((n) => typeof n === 'string');
} else if (typeof route?.redirect === 'string') {
if (route?.children?.length) {
const target = route.children.find((n) =>
[n.name, n.path, n.meta?.fullPath].some((m) => m === route?.redirect),
);
return findLastChild(target);
}
return route?.redirect;
}
return route;
};
const getRouteByName = (name: string) => router.getRoutes().find((n) => n.name === name);
//
const clickMenuItem = (menuItem: RouteRecordRaw) => {
const lastChild = findLastChild(menuItem);
console.log('lastChild', menuItem, lastChild);
const targetRoute = getRouteByName(lastChild?.name);
const { isExt, extOpenMode } = targetRoute?.meta || {};
if (isExt && extOpenMode === 1) {
window.open(lastChild?.path);
} else {
router.push({ name: lastChild?.name });
}
};
// 退
const doLogout = () => {
Modal.confirm({
title: '您确定要退出登录吗?',
icon: <QuestionCircleOutlined />,
centered: true,
onOk: async () => {
// rootadmin退
if (userStore.userInfo.phone !== '13553550634') {
// logout({})
await userStore.logout();
}
},
});
};
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.layout-header { .layout-header {
display: flex; display: flex;
position: sticky; position: sticky;
z-index: 10; z-index: 10;
top: 0; top: 0;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
height: @header-height; height: @header-height;
padding: 0 20px; padding: 0 20px;
* { * {
cursor: pointer; cursor: pointer;
}
} }
}
</style> </style>

View File

@ -4,7 +4,7 @@ import { useDictStore } from './dict';
import { useRouter, type RouteRecordRaw, useRoute } from 'vue-router'; import { useRouter, type RouteRecordRaw, useRoute } from 'vue-router';
import { store } from '@/store'; import { store } from '@/store';
import Api from '@/api/'; import Api from '@/api/';
import { ACCESS_TOKEN_KEY } from '@/enums/cacheEnum'; import { ACCESS_TOKEN_KEY, DOMAIN_KEY } from '@/enums/cacheEnum';
import { Storage } from '@/utils/Storage'; import { Storage } from '@/utils/Storage';
import { resetRouter } from '@/router'; import { resetRouter } from '@/router';
import { generateDynamicRoutes } from '@/router/helper/routeHelper'; import { generateDynamicRoutes } from '@/router/helper/routeHelper';
@ -21,12 +21,12 @@ export type MessageEvent = {
export const useUserStore = defineStore('user', () => { export const useUserStore = defineStore('user', () => {
let eventSource: EventSource | null = null; let eventSource: EventSource | null = null;
const token = ref(Storage.get(ACCESS_TOKEN_KEY, null)); const token = ref(Storage.get(ACCESS_TOKEN_KEY, null));
const name = ref('amdin'); const name = ref('admin');
const perms = ref<string[]>([]); const perms = ref<string[]>([]);
const menus = ref<RouteRecordRaw[]>([]); const menus = ref<RouteRecordRaw[]>([]);
const userInfo = ref<Partial<API.UserEntity>>({}); const userInfo = ref<Partial<API.UserEntity>>({});
const serverConnected = ref(true); const serverConnected = ref(true);
const domain = ref<API.DomainType>(Storage.get(DOMAIN_KEY, null));
watch(serverConnected, (val) => { watch(serverConnected, (val) => {
if (val) { if (val) {
initServerMsgListener(); initServerMsgListener();
@ -87,6 +87,7 @@ export const useUserStore = defineStore('user', () => {
// const ex = 7 * 24 * 60 * 60 * 1000; // const ex = 7 * 24 * 60 * 60 * 1000;
Storage.set(ACCESS_TOKEN_KEY, token.value); Storage.set(ACCESS_TOKEN_KEY, token.value);
}; };
/** 登录 */ /** 登录 */
const login = async (params: API.LoginDto) => { const login = async (params: API.LoginDto) => {
try { try {
@ -97,6 +98,7 @@ export const useUserStore = defineStore('user', () => {
return Promise.reject(error); return Promise.reject(error);
} }
}; };
/** 解锁屏幕 */ /** 解锁屏幕 */
const unlock = async (params: API.LoginDto) => { const unlock = async (params: API.LoginDto) => {
try { try {
@ -106,6 +108,17 @@ export const useUserStore = defineStore('user', () => {
return Promise.reject(error); return Promise.reject(error);
} }
}; };
/** 切换domain */
const changeDomain = (value) => {
domain.value = value;
Storage.set(DOMAIN_KEY, value);
setTimeout(() => {
// 刷新页面
window.location.reload();
}, 200);
};
/** 登录成功之后, 获取用户信息以及生成权限路由 */ /** 登录成功之后, 获取用户信息以及生成权限路由 */
const afterLogin = async () => { const afterLogin = async () => {
try { try {
@ -113,9 +126,12 @@ export const useUserStore = defineStore('user', () => {
useDictStore(); useDictStore();
// const wsStore = useWsStore(); // const wsStore = useWsStore();
const userInfoData = await accountProfile(); const userInfoData = await accountProfile();
userInfo.value = userInfoData; userInfo.value = userInfoData;
if (!Storage.get(DOMAIN_KEY)) {
domain.value = userInfoData.domain;
Storage.set(DOMAIN_KEY, domain.value);
}
await fetchPermsAndMenus(); await fetchPermsAndMenus();
initServerMsgListener(); initServerMsgListener();
} catch (error) { } catch (error) {
@ -158,6 +174,8 @@ export const useUserStore = defineStore('user', () => {
perms, perms,
menus, menus,
userInfo, userInfo,
domain,
changeDomain,
login, login,
unlock, unlock,
afterLogin, afterLogin,

View File

@ -123,6 +123,8 @@ export function request<T = any>(config: RequestOptions): Promise<BaseResponse<T
export async function request(_url: string | RequestOptions, _config: RequestOptions = {}) { export async function request(_url: string | RequestOptions, _config: RequestOptions = {}) {
const url = isString(_url) ? _url : _url.url; const url = isString(_url) ? _url : _url.url;
const config = isString(_url) ? _config : _url; const config = isString(_url) ? _config : _url;
const userStore = useUserStore();
try { try {
// 兼容 from data 文件上传的情况 // 兼容 from data 文件上传的情况
const { requestType, isReturnResult = true, ...rest } = config; const { requestType, isReturnResult = true, ...rest } = config;
@ -137,6 +139,7 @@ export async function request(_url: string | RequestOptions, _config: RequestOpt
headers: { headers: {
...rest.headers, ...rest.headers,
...(requestType === 'form' ? { 'Content-Type': 'multipart/form-data' } : {}), ...(requestType === 'form' ? { 'Content-Type': 'multipart/form-data' } : {}),
'sk-domain':userStore.domain
}, },
})) as AxiosResponse<BaseResponse>; })) as AxiosResponse<BaseResponse>;

View File

@ -21,264 +21,277 @@
</div> </div>
</template> </template>
<script setup <script setup lang="tsx">
lang="tsx"> import { useTable } from '@/components/core/dynamic-table';
import { useTable } from '@/components/core/dynamic-table'; import {
import { baseColumns,
baseColumns, type TableColumnItem,
type TableColumnItem, type TableListItem,
type TableListItem, } from './columns';
} from './columns'; import Api from '@/api/';
import Api from '@/api/'; import { onMounted, ref, type FunctionalComponent } from 'vue';
import { onMounted, ref, type FunctionalComponent } from 'vue'; import { useFormModal, useModal } from '@/hooks/useModal';
import { useFormModal, useModal } from '@/hooks/useModal'; import { Button, message } from 'ant-design-vue';
import { Button } from 'ant-design-vue'; import { formSchemas } from './formSchemas';
import { formSchemas } from './formSchemas'; import AttachmentManage from '@/components/business/attachment-manage/index.vue';
import AttachmentManage from '@/components/business/attachment-manage/index.vue'; import AttachmentUpload from '@/components/business/attachment-upload/index.vue';
import AttachmentUpload from '@/components/business/attachment-upload/index.vue'; import { useExportExcelModal, jsonToSheetXlsx } from '@/components/basic/excel';
import { useExportExcelModal, jsonToSheetXlsx } from '@/components/basic/excel'; import { MaterialsInOutEnum } from '@/enums/materialsInventoryEnum';
import { MaterialsInOutEnum } from '@/enums/materialsInventoryEnum'; import { formatToDate } from '@/utils/dateUtil';
import { formatToDate } from '@/utils/dateUtil'; import dayjs from 'dayjs';
defineOptions({ import fileDownload from 'js-file-download';
name: 'MaterialsInOut', defineOptions({
}); name: 'MaterialsInOut',
});
const [DynamicTable, dynamicTableInstance] = useTable({ formProps: { autoSubmitOnEnter: true } }); const [DynamicTable, dynamicTableInstance] = useTable({ formProps: { autoSubmitOnEnter: true } });
const [showModal] = useFormModal(); const [showModal] = useFormModal();
const exportExcelModal = useExportExcelModal(); const exportExcelModal = useExportExcelModal();
const [fnModal] = useModal(); const [fnModal] = useModal();
const isUploadPopupVisiable = ref(false); const isUploadPopupVisiable = ref(false);
let columns = ref<TableColumnItem[]>(); let columns = ref<TableColumnItem[]>();
onMounted(() => { onMounted(() => {
columns.value = [ columns.value = [
...baseColumns, ...baseColumns,
{ {
title: '附件', title: '附件',
width: 40, width: 40,
maxWidth: 40, maxWidth: 40,
hideInSearch: true, hideInSearch: true,
fixed: 'right', fixed: 'right',
dataIndex: 'files', dataIndex: 'files',
customRender: ({ record }) => <FilesRender {...record} />, customRender: ({ record }) => <FilesRender {...record} />,
}, },
{ {
title: '操作', title: '操作',
maxWidth: 80, maxWidth: 80,
width: 80, width: 80,
dataIndex: 'ACTION', dataIndex: 'ACTION',
hideInSearch: true, hideInSearch: true,
fixed: 'right', fixed: 'right',
actions: ({ record }) => [ actions: ({ record }) => [
{
icon: 'ant-design:edit-outlined',
tooltip: '编辑',
auth: {
perm: 'materials_inventory:history_in_out:update',
effect: 'disable',
},
onClick: () => openEditModal(record),
},
{
icon: 'ant-design:delete-outlined',
color: 'red',
tooltip: '删除此记录',
auth: 'materials_inventory:history_in_out:delete',
popConfirm: {
title: '你确定要删除吗?',
placement: 'left',
onConfirm: () => delRowConfirm(record.id),
},
},
{
icon: 'ant-design:cloud-upload-outlined',
tooltip: '上传附件',
onClick: () => openAttachmentUploadModal(record),
},
],
},
];
});
const openExportModal = () => {
exportExcelModal.openModal({
formSchemas: [
{ {
field: 'time', icon: 'ant-design:edit-outlined',
component: 'RangePicker', tooltip: '编辑',
label:'时间范围', auth: {
rules: [{ required: true }], perm: 'materials_inventory:history_in_out:update',
effect: 'disable',
},
onClick: () => openEditModal(record),
},
{
icon: 'ant-design:delete-outlined',
color: 'red',
tooltip: '删除此记录',
auth: 'materials_inventory:history_in_out:delete',
popConfirm: {
title: '你确定要删除吗?',
placement: 'left',
onConfirm: () => delRowConfirm(record.id),
},
},
{
icon: 'ant-design:cloud-upload-outlined',
tooltip: '上传附件',
onClick: () => openAttachmentUploadModal(record),
}, },
], ],
onOk: ({ filename, bookType }) => { },
const tableData: TableListItem[] = dynamicTableInstance.tableData; ];
let exportData: any[] = [] });
for (let item of tableData) {
exportData.push({ const openExportModal = async () => {
projectName: item.project?.name, const queryModel = dynamicTableInstance.queryFormRef?.formModel;
inOrOut: item.inOrOut === MaterialsInOutEnum.In ? '入库' : '出库', if(!queryModel?.time){
inventoryInOutNumber: item.inventoryInOutNumber, message.warning('请选择时间范围')
time: formatToDate(item.time), return;
company: item.product?.company?.name, }
productName: item.product?.name, const timeRange = (queryModel?.time ?? []).map(item => dayjs(item).format('YYYY-MM-DD'));
productSpecification: item.product?.productSpecification, const response = await Api.materialsInOut.materialsInoutExport({
unit: item.product?.unit?.label, time: timeRange ?? [], filename: `${timeRange[0]}-${timeRange[1]}`
quantity: item.quantity, });
unitPrice: parseFloat(item.unitPrice), fileDownload(response, `${timeRange[0]}-${timeRange[1]}.xls`);
amount: parseFloat(item.amount), // exportExcelModal.openModal({
agent: item.agent, // formSchemas: [
issuanceNumber: item.issuanceNumber, // {
remark: item.remark // field: 'time',
}) // component: 'RangePicker',
// label: '',
// rules: [{ required: true }],
// },
// ],
// onOk: async ({ filename, bookType, time }) => {
// const response = await Api.materialsInOut.materialsInoutExport({
// time: (time ?? []).map(item => dayjs(item).format('YYYY-MM-DD')), filename
// });
// fileDownload(response, `${filename}.xls`);
// const tableData: TableListItem[] = dynamicTableInstance.tableData;
// let exportData: any[] = []
// for (let item of tableData) {
// exportData.push({
// projectName: item.project?.name,
// inOrOut: item.inOrOut === MaterialsInOutEnum.In ? '' : '',
// inventoryInOutNumber: item.inventoryInOutNumber,
// time: formatToDate(item.time),
// company: item.product?.company?.name,
// productName: item.product?.name,
// productSpecification: item.product?.productSpecification,
// unit: item.product?.unit?.label,
// quantity: item.quantity,
// unitPrice: parseFloat(item.unitPrice),
// amount: parseFloat(item.amount),
// agent: item.agent,
// issuanceNumber: item.issuanceNumber,
// remark: item.remark
// })
// }
// jsonToSheetXlsx({
// data: exportData,
// header: {
// inOrOut: '/',
// inventoryInOutNumber: '',
// time: '',
// projectName: '',
// company: '',
// productName: '',
// productSpecification: "",
// unit: '',
// quantity: '',
// unitPrice: '',
// amount: '',
// agent: '',
// issuanceNumber: '',
// remark: ''
// },
// filename,
// write2excelOpts:
// { bookType: 'xls' },
// json2sheetOpts: {
// //
// header: [
// 'inOrOut', 'inventoryInOutNumber', 'time', 'projectName', 'company', 'productName', 'productSpecification', 'unit', 'quantity',
// 'unitPrice', 'amount', 'agent', 'issuanceNumber', 'remark'],
// },
// });
// },
// });
};
const openAttachmentUploadModal = async (record: TableListItem) => {
isUploadPopupVisiable.value = true;
fnModal.show({
width: 800,
title: `上传附件: ${record.id}`,
content: () => {
return (
<AttachmentUpload
onClose={handleUploadClose}
bussinessModule="materialsInOut"
bussinessRecordId={record.id}
afterUploadCallback={(files) => {
afterUploadCallback(files, record.id);
}}
></AttachmentUpload>
);
},
destroyOnClose: true,
open: isUploadPopupVisiable.value,
footer: null,
});
};
const handleUploadClose = (hasSuccess: boolean) => {
fnModal.hide();
isUploadPopupVisiable.value = false;
};
const afterUploadCallback = async (
files: { filename: { path: string; id: number } }[],
id: number,
) => {
await Api.materialsInOut.materialsInOutUpdate(
{ id },
{ fileIds: files.map((item) => item.filename.id) },
);
dynamicTableInstance?.reload();
};
/**
* @description 打开新增/编辑弹窗
*/
const openEditModal = async (record: Partial<TableListItem>) => {
const [formRef] = await showModal({
modalProps: {
title: `${record.id ? '编辑' : '新增'}出入库记录`,
width: '50%',
onFinish: async (values) => {
const params = {
...values,
// signingDate: formatToDate(values.signingDate),
// deliveryDeadline: formatToDate(values.deliveryDeadline),
};
if (record.id) {
await Api.materialsInOut.materialsInOutUpdate(
{ id: record.id },
params as API.MaterialsInOutUpdateDto,
);
} else {
await Api.materialsInOut.materialsInOutCreate(params as API.MaterialsInOutDto);
} }
jsonToSheetXlsx({ dynamicTableInstance?.reload();
data: exportData,
header: {
inOrOut: '出/入库',
inventoryInOutNumber: '出入库单号',
time: '时间',
projectName: '项目',
company: '公司',
productName: '产品名',
productSpecification: "产品规格",
unit: '单位',
quantity: '数量',
unitPrice: '单价',
amount: '金额',
agent: '经办人',
issuanceNumber: '领料单号',
remark: '备注'
},
filename,
write2excelOpts: {
bookType,
},
json2sheetOpts: {
//
header: [
'inOrOut', 'inventoryInOutNumber', 'time', 'projectName', 'company', 'productName', 'productSpecification', 'unit', 'quantity',
'unitPrice', 'amount', 'agent', 'issuanceNumber', 'remark'],
},
});
}, },
},
formProps: {
labelWidth: 100,
schemas: formSchemas(!!record.id),
},
});
//
if (record.id) {
const info = await Api.materialsInOut.materialsInOutInfo({ id: record.id });
formRef?.setFieldsValue({
...info,
}); });
}; }
const openAttachmentUploadModal = async (record: TableListItem) => { };
isUploadPopupVisiable.value = true; const delRowConfirm = async (record) => {
fnModal.show({ await Api.materialsInOut.materialsInOutDelete({ id: record });
width: 800, dynamicTableInstance?.reload();
title: `上传附件: ${record.id}`, };
content: () => {
return (
<AttachmentUpload
onClose={handleUploadClose}
bussinessModule="materialsInOut"
bussinessRecordId={record.id}
afterUploadCallback={(files) => {
afterUploadCallback(files, record.id);
}}
></AttachmentUpload>
);
},
destroyOnClose: true,
open: isUploadPopupVisiable.value,
footer: null,
});
};
const handleUploadClose = (hasSuccess: boolean) => {
fnModal.hide();
isUploadPopupVisiable.value = false;
};
const afterUploadCallback = async (
files: { filename: { path: string; id: number } }[],
id: number,
) => {
await Api.materialsInOut.materialsInOutUpdate(
{ id },
{ fileIds: files.map((item) => item.filename.id) },
);
dynamicTableInstance?.reload();
};
/** const FilesRender: FunctionalComponent<TableListItem> = (materialsInOut: TableListItem) => {
* @description 打开新增/编辑弹窗 const [fnModal] = useModal();
*/ return (
const openEditModal = async (record: Partial<TableListItem>) => { <Button
const [formRef] = await showModal({ type="link"
modalProps: { onClick={() => {
title: `${record.id ? '编辑' : '新增'}出入库记录`, openFilesManageModal(fnModal, materialsInOut);
width: '50%', }}
onFinish: async (values) => { >
const params = { {materialsInOut.files?.length || 0}
...values, </Button>
// signingDate: formatToDate(values.signingDate), );
// deliveryDeadline: formatToDate(values.deliveryDeadline), };
};
if (record.id) {
await Api.materialsInOut.materialsInOutUpdate(
{ id: record.id },
params as API.MaterialsInOutUpdateDto,
);
} else {
await Api.materialsInOut.materialsInOutCreate(params as API.MaterialsInOutDto);
}
dynamicTableInstance?.reload();
},
},
formProps: {
labelWidth: 100,
schemas: formSchemas(!!record.id),
},
});
//
if (record.id) {
const info = await Api.materialsInOut.materialsInOutInfo({ id: record.id });
formRef?.setFieldsValue({
...info,
});
}
};
const delRowConfirm = async (record) => {
await Api.materialsInOut.materialsInOutDelete({ id: record });
dynamicTableInstance?.reload();
};
const FilesRender: FunctionalComponent<TableListItem> = (materialsInOut: TableListItem) => { const openFilesManageModal = (fnModal, tableData: TableListItem) => {
const [fnModal] = useModal(); const fileIds = tableData.files?.map((item) => item.id) || [];
return ( fnModal.show({
<Button width: 1200,
type="link" title: `附件管理`,
onClick={() => { content: () => {
openFilesManageModal(fnModal, materialsInOut); return (
}} <AttachmentManage
> fileIds={fileIds}
{materialsInOut.files?.length || 0} onDelete={(unlinkIds) => unlinkAttachments(tableData.id, unlinkIds)}
</Button> ></AttachmentManage>
); );
}; },
destroyOnClose: true,
const openFilesManageModal = (fnModal, tableData: TableListItem) => { footer: null,
const fileIds = tableData.files?.map((item) => item.id) || []; });
fnModal.show({ };
width: 1200, const unlinkAttachments = async (id: number, unlinkIds: number[]) => {
title: `附件管理`, await Api.materialsInOut.unlinkAttachments({ id }, { fileIds: unlinkIds });
content: () => { dynamicTableInstance?.reload();
return ( };
<AttachmentManage
fileIds={fileIds}
onDelete={(unlinkIds) => unlinkAttachments(tableData.id, unlinkIds)}
></AttachmentManage>
);
},
destroyOnClose: true,
footer: null,
});
};
const unlinkAttachments = async (id: number, unlinkIds: number[]) => {
await Api.materialsInOut.unlinkAttachments({ id }, { fileIds: unlinkIds });
dynamicTableInstance?.reload();
};
</script> </script>
<style lang="less" <style lang="less" scoped></style>
scoped></style>

View File

@ -18,139 +18,154 @@
</div> </div>
</template> </template>
<script setup <script setup lang="tsx">
lang="tsx"> import { useTable } from '@/components/core/dynamic-table';
import { useTable } from '@/components/core/dynamic-table'; import { baseColumns, type TableColumnItem, type TableListItem } from './columns';
import { baseColumns, type TableColumnItem, type TableListItem } from './columns'; import Api from '@/api/';
import Api from '@/api/'; import { onMounted, ref, unref } from 'vue';
import { onMounted, ref, unref } from 'vue'; import { useFormModal } from '@/hooks/useModal';
import { useFormModal } from '@/hooks/useModal'; import { exportSchemas } from './exportSchema';
import { exportSchemas } from './exportSchema'; import dayjs from 'dayjs';
import dayjs from 'dayjs'; import fileDownload from 'js-file-download';
import fileDownload from 'js-file-download'; import { message } from 'ant-design-vue';
import { message } from 'ant-design-vue'; import { useExportExcelModal } from '@/components/basic/excel';
defineOptions({ defineOptions({
name: 'MaterialsInventory', name: 'MaterialsInventory',
}); });
const [DynamicTable, dynamicTableInstance] = useTable(); const [DynamicTable, dynamicTableInstance] = useTable();
const [showExportModal] = useFormModal(); const [showExportModal] = useFormModal();
let columns = ref<TableColumnItem[]>(); const exportExcelModal = useExportExcelModal();
onMounted(() => { let columns = ref<TableColumnItem[]>();
columns.value = [ onMounted(() => {
...baseColumns, columns.value = [
...baseColumns,
// { // {
// title: '', // title: '',
// maxWidth: 60, // maxWidth: 60,
// width: 60, // width: 60,
// minWidth: 60, // minWidth: 60,
// dataIndex: 'ACTION', // dataIndex: 'ACTION',
// hideInSearch: true, // hideInSearch: true,
// actions: ({ record }) => [ // actions: ({ record }) => [
// { // {
// icon: 'ant-design:edit-outlined', // icon: 'ant-design:edit-outlined',
// tooltip: '', // tooltip: '',
// auth: { // auth: {
// perm: 'app:materials_inventory:update', // perm: 'app:materials_inventory:update',
// effect: 'disable', // effect: 'disable',
// }, // },
// onClick: () => openEditModal(record), // onClick: () => openEditModal(record),
// },
// // {
// // icon: 'ant-design:delete-outlined',
// // color: 'red',
// // tooltip: '',
// // auth: 'app:materials_inventory:delete',
// // popConfirm: {
// // title: '',
// // placement: 'left',
// // onConfirm: () => delRowConfirm(record.id),
// // },
// // },
// ],
// },
];
});
const exportMI = async () => {
const { time } = unref<{ time: string; projectId: number }>(
dynamicTableInstance?.queryFormRef?.formModel as { time: string; projectId: number },
);
const [formRef] = await showExportModal({
modalProps: {
title: `导出条件选择`,
width: '50%',
okText: '导出',
onFinish: async (values) => {
const response = await Api.materialsInventory.materialsInventoryExport(values);
const { time } = values;
fileDownload(response, `${dayjs(time).format('YYYY.MM.盘点表')}.xls`);
},
},
formProps: {
labelWidth: 100,
schemas: exportSchemas,
},
});
// auto fill export time fields
if (time) {
formRef?.setFieldsValue({
time,
});
}
};
/**
* @description 打开新增/编辑弹窗
*/
const openEditModal = async (record: Partial<TableListItem>) => {
message.warning(
<div>
暂不支持编辑功能,等待后期需求确认
<div>库存数量是由出入库记录自动计算得出的库存单价为入库的单价</div>
</div>
);
// const [formRef] = await showModal({
// modalProps: {
// title: `${record.id ? '' : ''}`,
// width: '50%',
// onFinish: async (values) => {
// const params = {
// ...values,
// signingDate: formatToDate(values.signingDate),
// deliveryDeadline: formatToDate(values.deliveryDeadline),
// };
// if (record.id) {
// await Api.contract.contractUpdate({ id: record.id }, params);
// } else {
// await Api.contract.contractCreate(params);
// }
// dynamicTableInstance?.reload();
// }, // },
// }, // // {
// formProps: { // // icon: 'ant-design:delete-outlined',
// labelWidth: 100, // // color: 'red',
// schemas: contractSchemas(contractTypes.value), // // tooltip: '',
// }, // // auth: 'app:materials_inventory:delete',
// }); // // popConfirm: {
// // // // title: '',
// if (record.id) { // // placement: 'left',
// const info = await Api.contract.contractInfo({ id: record.id }); // // onConfirm: () => delRowConfirm(record.id),
// formRef?.setFieldsValue({ // // },
// ...info, // // },
// }); // ],
// } // },
}; ];
const delRowConfirm = async (record) => { });
await Api.contract.contractDelete({ id: record }); const exportMI = async () => {
dynamicTableInstance?.reload(); const { time } = unref<{ time: string; projectId: number }>(
}; dynamicTableInstance?.queryFormRef?.formModel as { time: string; projectId: number },
);
exportExcelModal.openModal({
formSchemas: [
{
field: 'time',
component: 'RangePicker',
label: '时间范围',
rules: [{ required: true }],
},
],
onOk: async ({ filename, bookType, time }) => {
const response = await Api.materialsInventory.materialsInventoryExport({
time: (time ?? []).map(item => dayjs(item).format('YYYY-MM-DD')), filename
});
fileDownload(response, `${filename}.xls`);
}
});
// const [formRef] = await showExportModal({
// modalProps: {
// title: ``,
// width: '50%',
// okText: '',
// onFinish: async (values) => {
// const response = await Api.materialsInventory.materialsInventoryExport(values);
// const { time } = values;
// fileDownload(response, `${dayjs(time).format('YYYY.MM.')}.xls`);
// },
// },
// formProps: {
// labelWidth: 100,
// schemas: exportSchemas,
// },
// });
// auto fill export time fields
// if (time) {
// formRef?.setFieldsValue({
// time,
// });
// }
};
/**
* @description 打开新增/编辑弹窗
*/
const openEditModal = async (record: Partial<TableListItem>) => {
message.warning(
<div>
暂不支持编辑功能,等待后期需求确认
<div>库存数量是由出入库记录自动计算得出的库存单价为入库的单价</div>
</div>
);
// const [formRef] = await showModal({
// modalProps: {
// title: `${record.id ? '' : ''}`,
// width: '50%',
// onFinish: async (values) => {
// const params = {
// ...values,
// signingDate: formatToDate(values.signingDate),
// deliveryDeadline: formatToDate(values.deliveryDeadline),
// };
// if (record.id) {
// await Api.contract.contractUpdate({ id: record.id }, params);
// } else {
// await Api.contract.contractCreate(params);
// }
// dynamicTableInstance?.reload();
// },
// },
// formProps: {
// labelWidth: 100,
// schemas: contractSchemas(contractTypes.value),
// },
// });
// //
// if (record.id) {
// const info = await Api.contract.contractInfo({ id: record.id });
// formRef?.setFieldsValue({
// ...info,
// });
// }
};
const delRowConfirm = async (record) => {
await Api.contract.contractDelete({ id: record });
dynamicTableInstance?.reload();
};
</script> </script>
<style lang="less" <style lang="less" scoped></style>
scoped></style>
import dayjs from 'dayjs'; import fileDownload from 'js-file-download'; import type { TableQueryItem import dayjs from 'dayjs'; import fileDownload from 'js-file-download'; import type { TableQueryItem
} from '../in-out/columns'; import { exportSchemas } from '../in-out/exportSchema'; import dayjs } from '../in-out/columns'; import { exportSchemas } from '../in-out/exportSchema'; import dayjs
from 'dayjs'; import fileDownload from 'js-file-download'; import type { TableQueryItem } from from 'dayjs'; import fileDownload from 'js-file-download'; import type { TableQueryItem } from