调整一些菜单

This commit is contained in:
z9130 2024-08-04 22:44:31 +08:00
parent 34de177171
commit e00ff6a51d
17 changed files with 1225 additions and 118 deletions

View File

@ -2,6 +2,18 @@ import type { RouteMeta } from 'vue-router'
import ElegantVueRouter from '@elegant-router/vue/vite'
import type { RouteKey } from '@elegant-router/types'
function generateEditPath(key) {
const editSuffix = '_edit';
if (key.includes(editSuffix)) {
const baseKey = key.replace(editSuffix, '');
const path = `/${baseKey.replace(/_/g, '/')}/edit/:id?`;
return path;
}
return null; // 如果key不包含_edit返回null或你希望的默认值
}
export function setupElegantRouter() {
return ElegantVueRouter({
layouts: {
@ -22,6 +34,7 @@ export function setupElegantRouter() {
'document_antd',
],
},
routePathTransformer(routeName, routePath) {
const key = routeName as RouteKey
@ -33,29 +46,35 @@ export function setupElegantRouter() {
return `/login/:module(${moduleReg})?`
}
if (key === 'meeting_edit') {
return '/meeting/edit/:id?'
let newPath = generateEditPath(key)
if (newPath) {
return newPath
}
if (key === 'contract_approval_edit') {
return '/contract/approval/edit/:id?'
}
// if (key === 'meeting_edit') {
// return '/meeting/edit/:id?'
// }
if (key === 'contract_business_edit') {
return '/contract/business/edit/:id?'
}
// if (key === 'contract_approval_edit') {
// return '/contract/approval/edit/:id?'
// }
if (key === 'contract_declaration_edit') {
return '/contract/declaration/edit/:id?'
}
// if (key === 'contract_business_edit') {
// return '/contract/business/edit/:id?'
// }
if (key === 'contract_archive_edit') {
return '/contract/archive/edit/:id?'
}
// if (key === 'contract_declaration_edit') {
// return '/contract/declaration/edit/:id?'
// }
if (key === 'contract_company_edit') {
return '/contract/company/edit/:id?'
}
// if (key === 'contract_archive_edit') {
// return '/contract/archive/edit/:id?'
// }
// if (key === 'contract_company_edit') {
// return '/contract/company/edit/:id?'
// }
return routePath
},

View File

@ -77,20 +77,20 @@ export const alovaInstance = createAlova({
statesHook: vueHook,
requestAdapter: fetchAdapter(),
/** 设置缓存状态:不开启 */
// cacheFor: null,
cacheFor: {
// 统一设置POST的缓存模式
GET: {
mode: 'restore',
expire: 60 * 10 * 1000
},
POST: {
mode: 'restore',
expire: 60 * 10 * 1000
},
// 统一设置HEAD请求的缓存模式
HEAD: 60 * 10 * 1000
},
cacheFor: null,
// cacheFor: {
// // 统一设置POST的缓存模式
// GET: {
// mode: 'restore',
// expire: 60 * 10 * 1000
// },
// POST: {
// mode: 'restore',
// expire: 60 * 10 * 1000
// },
// // 统一设置HEAD请求的缓存模式
// HEAD: 60 * 10 * 1000
// },
/** 请求拦截器 */
beforeRequest: onAuthRequired(method => {

View File

@ -3,6 +3,7 @@ import { computed } from 'vue';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route';
import { useTabStore } from '@/store/modules/tab';
defineOptions({
name: 'GlobalContent'
@ -20,6 +21,7 @@ withDefaults(defineProps<Props>(), {
const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const tabStore = useTabStore();
const transitionName = computed(() => (themeStore.page.animate ? themeStore.page.animateMode : ''));
</script>
@ -36,7 +38,7 @@ const transitionName = computed(() => (themeStore.page.animate ? themeStore.page
<component
:is="Component"
v-if="appStore.reloadFlag"
:key="route.path"
:key="tabStore.getTabIdByRoute(route)"
:class="{ 'p-16px': showPadding }"
class="flex-grow bg-layout transition-300"
/>

View File

@ -21,8 +21,9 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"),
login: () => import("@/views/_builtin/login/index.vue"),
about: () => import("@/views/about/index.vue"),
"bussiness-trip_edit": () => import("@/views/bussiness-trip/edit/index.vue"),
"bussiness-trip_edit": () => import("@/views/bussiness-trip/edit/[id].vue"),
"bussiness-trip_home": () => import("@/views/bussiness-trip/home/index.vue"),
"bussiness-trip_todo": () => import("@/views/bussiness-trip/todo/index.vue"),
canteen_collect: () => import("@/views/canteen/collect/index.vue"),
canteen_config: () => import("@/views/canteen/config/index.vue"),
canteen_menu: () => import("@/views/canteen/menu/index.vue"),
@ -68,6 +69,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
"office-supplies_audit": () => import("@/views/office-supplies/audit/index.vue"),
"office-supplies_inventory": () => import("@/views/office-supplies/inventory/index.vue"),
"office-supplies_purchase": () => import("@/views/office-supplies/purchase/index.vue"),
supervise_edit: () => import("@/views/supervise/edit/[id].vue"),
supervise_list: () => import("@/views/supervise/list/index.vue"),
supervise_statistics: () => import("@/views/supervise/statistics/index.vue"),
system_dict: () => import("@/views/system/dict/index.vue"),

View File

@ -60,18 +60,13 @@ export const generatedRoutes: GeneratedRoute[] = [
icon: 'icon-park-outline:train',
order: 28
},
redirect: {
name: 'bussiness-trip_home'
},
children: [
{
name: 'bussiness-trip_edit',
path: '/bussiness-trip/edit',
path: '/bussiness-trip/edit/:id',
component: 'view.bussiness-trip_edit',
meta: {
title: '出差录入',
hideInMenu: true,
activeMenu: 'bussiness-trip'
title: '出差填报'
}
},
{
@ -79,9 +74,15 @@ export const generatedRoutes: GeneratedRoute[] = [
path: '/bussiness-trip/home',
component: 'view.bussiness-trip_home',
meta: {
title: '出差管理',
hideInMenu: true,
activeMenu: 'bussiness-trip'
title: '出差查询'
}
},
{
name: 'bussiness-trip_todo',
path: '/bussiness-trip/todo',
component: 'view.bussiness-trip_todo',
meta: {
title: '出差提示'
}
}
]
@ -692,10 +693,8 @@ export const generatedRoutes: GeneratedRoute[] = [
path: '/meeting/edit/:id?',
component: 'view.meeting_edit',
meta: {
title: '会议编辑',
hideInMenu: true,
activeMenu: 'meeting',
order: 999
title: '会议填报',
order: 20
}
},
{
@ -703,9 +702,8 @@ export const generatedRoutes: GeneratedRoute[] = [
path: '/meeting/home',
component: 'view.meeting_home',
meta: {
title: '会议管理',
hideInMenu: true,
activeMenu: 'meeting'
title: '会议查询',
order: 30
}
}
]
@ -852,13 +850,23 @@ export const generatedRoutes: GeneratedRoute[] = [
order: 20
},
children: [
{
name: 'supervise_edit',
path: '/supervise/edit/:id',
component: 'view.supervise_edit',
meta: {
title: '立项填报',
order: 10
}
},
{
name: 'supervise_list',
path: '/supervise/list',
component: 'view.supervise_list',
meta: {
title: '督办列表',
icon: 'icon-park-outline:list-view'
title: '立项查询',
icon: 'icon-park-outline:list-view',
order: 20
}
},
{
@ -867,7 +875,8 @@ export const generatedRoutes: GeneratedRoute[] = [
component: 'view.supervise_statistics',
meta: {
title: '报表汇总',
icon: 'icon-park-outline:analysis'
icon: 'icon-park-outline:analysis',
order:30
}
}
]

View File

@ -44,7 +44,7 @@ function transformElegantRouteToVueRoute(
function getLayoutName(component: string) {
const layout = component.replace(LAYOUT_PREFIX, '');
if(!layouts[layout]) {
if (!layouts[layout]) {
throw new Error(`Layout component "${layout}" not found`);
}
@ -58,7 +58,7 @@ function transformElegantRouteToVueRoute(
function getViewName(component: string) {
const view = component.replace(VIEW_PREFIX, '');
if(!views[view]) {
if (!views[view]) {
throw new Error(`View component "${view}" not found`);
}
@ -97,7 +97,7 @@ function transformElegantRouteToVueRoute(
if (component) {
if (isSingleLevelRoute(route)) {
const { layout, view } = getSingleLevelRouteComponent(component);
const singleLevelRoute: RouteRecordRaw = {
path,
component: layouts[layout],
@ -110,40 +110,40 @@ function transformElegantRouteToVueRoute(
} as RouteRecordRaw
]
};
return [singleLevelRoute];
}
if (isLayout(component)) {
const layoutName = getLayoutName(component);
vueRoute.component = layouts[layoutName];
}
if (isView(component)) {
const viewName = getViewName(component);
vueRoute.component = views[viewName];
}
}
} catch (error: any) {
console.error(`Error transforming route "${route.name}": ${error.toString()}`);
return [];
}
// add redirect to child
if (children?.length && !vueRoute.redirect) {
vueRoute.redirect = {
name: children[0].name
};
}
if (children?.length) {
const childRoutes = children.flatMap(child => transformElegantRouteToVueRoute(child, layouts, views));
if(isFirstLevelRoute(route)) {
if (isFirstLevelRoute(route)) {
vueRoute.children = childRoutes;
} else {
vueRoutes.push(...childRoutes);
@ -178,8 +178,9 @@ const routeMap: RouteMap = {
"500": "/500",
"about": "/about",
"bussiness-trip": "/bussiness-trip",
"bussiness-trip_edit": "/bussiness-trip/edit",
"bussiness-trip_edit": "/bussiness-trip/edit/:id",
"bussiness-trip_home": "/bussiness-trip/home",
"bussiness-trip_todo": "/bussiness-trip/todo",
"canteen": "/canteen",
"canteen_collect": "/canteen/collect",
"canteen_config": "/canteen/config",
@ -251,6 +252,7 @@ const routeMap: RouteMap = {
"office-supplies_inventory": "/office-supplies/inventory",
"office-supplies_purchase": "/office-supplies/purchase",
"supervise": "/supervise",
"supervise_edit": "/supervise/edit/:id",
"supervise_list": "/supervise/list",
"supervise_statistics": "/supervise/statistics",
"system": "/system",

View File

@ -17,6 +17,7 @@ import {
getDefaultHomeTab,
getFixedTabIds,
getTabByRoute,
getTabIdByRoute,
isTabInTabs,
updateTabByI18nKey,
updateTabsByI18nKey
@ -288,6 +289,8 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
setTabLabel,
resetTabLabel,
isTabRetain,
updateTabsByLocale
updateTabsByLocale,
getTabIdByRoute,
cacheTabs
};
});

View File

@ -34,8 +34,9 @@ declare module "@elegant-router/types" {
"500": "/500";
"about": "/about";
"bussiness-trip": "/bussiness-trip";
"bussiness-trip_edit": "/bussiness-trip/edit";
"bussiness-trip_edit": "/bussiness-trip/edit/:id";
"bussiness-trip_home": "/bussiness-trip/home";
"bussiness-trip_todo": "/bussiness-trip/todo";
"canteen": "/canteen";
"canteen_collect": "/canteen/collect";
"canteen_config": "/canteen/config";
@ -107,6 +108,7 @@ declare module "@elegant-router/types" {
"office-supplies_inventory": "/office-supplies/inventory";
"office-supplies_purchase": "/office-supplies/purchase";
"supervise": "/supervise";
"supervise_edit": "/supervise/edit/:id";
"supervise_list": "/supervise/list";
"supervise_statistics": "/supervise/statistics";
"system": "/system";
@ -202,6 +204,7 @@ declare module "@elegant-router/types" {
| "about"
| "bussiness-trip_edit"
| "bussiness-trip_home"
| "bussiness-trip_todo"
| "canteen_collect"
| "canteen_config"
| "canteen_menu"
@ -247,6 +250,7 @@ declare module "@elegant-router/types" {
| "office-supplies_audit"
| "office-supplies_inventory"
| "office-supplies_purchase"
| "supervise_edit"
| "supervise_list"
| "supervise_statistics"
| "system_dict"
@ -417,3 +421,5 @@ declare module "@elegant-router/types" {
*/
type ElegantRoute = GeneratedRoute | CustomRoute;
}
e;
}

View File

@ -73,7 +73,6 @@ export class FileUploader {
};
initiateUpload = () => {
debugger
this.uploadComplete.value = false;
this.uploadInProgress.value = false;
@ -109,11 +108,11 @@ export class FileUploader {
let newFileList: any[] = [];
for (const file of fileList) {
newFileList.push({
fileid: file.fileUuid,
id: file.fileUuid,
batchId: file.fileUuid,
name: file.name,
name: file.name || file.fileName,
status: "finished",
url: file.url,
url: file.fileUrl,
})
}
this.fileList.value = newFileList;

View File

@ -0,0 +1,361 @@
<script setup lang="ts">
import { reactive, ref, computed, onMounted } from "vue";
import { useDialog, useMessage } from "naive-ui";
import type { UploadCustomRequestOptions, UploadFileInfo, UploadInst } from "naive-ui";
import {
getMeetingList,
getAddressorList,
saveMeeting,
} from "@/api/office/meeting";
import { useVxeTable } from "@/hooks/common/vxeTable";
import { useRoute } from "vue-router";
import { useRequest } from "alova/client";
import { DICT_TYPE, getDictDefaultObj, getDictObj, getDictOptions } from "@/utils/dict";
import { uploadFile, uploadFiles, downloadFile } from "@/api/system/file";
import ChooseUserModal from "@/views/user-center/ChooseUserModal.vue";
import { router } from "@/router";
import { useTabStore } from "@/store/modules/tab";
import { FileUploader } from "@/utils/file";
import { FileSource } from "@/enums";
import { useUserStore } from "@/store/modules/user";
import dayjs from "dayjs";
import { useRouterPush } from "@/hooks/common/router";
const { routerPushByKey, routerPush } = useRouterPush();
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
const { xGridRef, gridProps, triggerProxy } = useVxeTable({ ref: 'xGridRef' });
const PrimaryKey = "guid"
const message = useMessage();
const dialog = useDialog();
const route = useRoute();
const id = route.params.id;
const tabStore = useTabStore();
const chooseUserModalRef = ref();
const spokespersonModalRef = ref();
const selectField = ref("");
const uploadRef = ref<UploadInst | null>(null);
const fileUploader = new FileUploader({
uploadRef,
data: { source: FileSource.Erp },
onSuccess: (fileList: any[]) => {
console.log('[ fileList ] >', fileList)
handleSubmit('submit')
},
onError: (err: any) => {
console.log('[ err ] >', err)
},
})
/** 必填字段校验 */
const requiredFieldRules: any = {
"timeRanger": { required: true, message: '请选择出差时间' },
"days": { required: true, message: '请输入出差天数' },
"place": { required: true, message: '请输入出差地点' },
}
/** 必填字段校验,如果是必填字段则返回对应message */
function isRequired(field: string): string {
let rule = requiredFieldRules[field];
if (rule && rule.required) {
return rule.message || `${field}不可为空`;
}
return ""
}
/** Hooks 数据请求 - 会议 */
const currData = ref<any>({
phone: userInfo.value.phone
});
const { send, onSuccess, loading } = useRequest(Apis.erp.get_ccsq_page({ params: { guid: id } }), {
immediate: false,
});
onSuccess((res: any) => {
console.log(res);
if (res.data && res.data?.rows && res.data?.rows.length > 0) {
currData.value = res.data?.rows[0] || {};
try {
currData.value.timeRanger = [dayjs(currData.value.starttime).format('YYYY-MM-DD hh:mm'), dayjs(currData.value.endtime).format('YYYY-MM-DD hh:mm')];
} catch (err) {
console.log(err)
}
//
if (currData.value?.fileUuid) {
Apis.common.get_attachment_list({
params: {
uuid: currData.value?.fileUuid,
},
}).then((res: any) => {
console.log(res);
fileUploader.setFileList(res.rows);
console.log(fileUploader.fileList.value)
});
}
console.log("当前数据:", currData.value);
} else {
dialog.error({
title: '提示',
content: '当出差记录不存在',
positiveText: '返回上一层',
closable: false,
maskClosable: false,
onPositiveClick: () => {
back()
}
})
}
});
/**
* 提交
*/
async function handleSubmit(type: "submit" | "upload") {
//
for (const field of Object.keys(requiredFieldRules)) {
if (!currData.value[field]) {
message.error(isRequired(field));
return
}
}
// let loading = message.loading("");
console.log(currData.value);
try {
if (type === "submit") {
//
if (fileUploader.fileList.value && fileUploader.fileList.value.length) {
let fileUuids = fileUploader.fileList.value.map((item) => item.batchId);
currData.value.fileUuid = fileUuids.join(',')
} else {
currData.value.fileUuid = ''
}
let values = JSON.parse(JSON.stringify(currData.value))
values.starttime = values.timeRanger?.[0];
values.endtime = values.timeRanger?.[1];
values.timeRanger = undefined;
values.applyName = userStore.userInfo.gwmc;
values.applyId = userStore.userInfo.userId;
values.applyTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
await Apis.erp.post_ccsq_save({
data: values
});
message.success("提交成功");
back();
} else {
fileUploader.initiateUpload();
}
} catch (error) {
console.error(error)
} finally {
// loading.destroy()
}
}
/**
* 页面返回并关闭tab
*/
function back() {
router.removeRoute("bussiness-trip_home")
tabStore.removeActiveTab();
}
onMounted(async () => {
if (id) {
await send();
}
});
const containerRef = ref<HTMLElement | undefined>(undefined);
function calculateDaysBetweenTimestamps(timestamp1, timestamp2) {
const date1 = new Date(timestamp1);
const date2 = new Date(timestamp2);
const timeDifference = Math.abs(date2 - date1);
const daysDifference = timeDifference / (24 * 60 * 60 * 1000);
const roundedDaysDifference = Math.round(daysDifference * 2) / 2;
return roundedDaysDifference;
}
function handleUpdateFormattedValue(value) {
const startTimestamp = new Date(value[0]).getTime();
const endTimestamp = new Date(value[1]).getTime();
const daysBetween = calculateDaysBetweenTimestamps(startTimestamp, endTimestamp);
currData.value.days = daysBetween;
}
function handleBack() {
dialog.warning({
title: "提示",
content: `是否确认返回上一页面?`,
positiveText: "确认",
negativeText: "取消",
onPositiveClick: async () => {
back();
},
});
}
</script>
<template>
<div ref="containerRef"
class="flex-col-stretch relative gap-16px overflow-hidden bg-white px-4 py-2 lt-sm:overflow-auto">
<div>
<n-affix class="z-20 w-full bg-white py-1" :trigger-top="10" position="absolute" :listen-to="() => containerRef">
<n-space>
<n-button type="primary" size="small" @click="handleSubmit('upload')">
<template #icon>
<icon-mdi-send />
</template>
提交
</n-button>
<n-button size="small" @click="handleBack()">
<template #icon>
<icon-mdi-undo-variant />
</template>
返回
</n-button>
</n-space>
</n-affix>
</div>
<n-space vertical class="overflow-auto">
<div class="h-20px"></div>
<n-card size="small">
<template #header>
<div class="flex flex-row items-center">
<span>基本信息</span>
</div>
</template>
<n-spin :show="loading">
<n-descriptions label-placement="left" bordered :column="6" label-class="w-120px!" size="small">
<n-descriptions-item :span="6">
<template #label>
出差时间
<span v-if="isRequired('timeRanger')" class="text-red">*</span>
</template>
<NDatePicker v-model:formatted-value="currData.timeRanger" type="datetimerange" :close-on-select="true"
placeholder="请选择出差时间" format="yyyy-MM-dd HH:mm" valueFormat="yyyy-MM-dd HH:mm"
:timePickerProps="{ format: 'hh:mm' }" @update-formatted-value="handleUpdateFormattedValue">
</NDatePicker>
</n-descriptions-item>
<n-descriptions-item :span="6">
<template #label>
天数
<span v-if="isRequired('days')" class="text-red">*</span>
</template>
<div class="">
<n-input-number class="flex-1" v-model:value="currData.days" placeholder="请输入出差天数"></n-input-number>
</div>
</n-descriptions-item>
<n-descriptions-item :span="6">
<template #label>
出差地点
<span v-if="isRequired('place')" class="text-red">*</span>
</template>
<div class="">
<n-input class="flex-1" type="textarea" v-model:value="currData.place" placeholder="请输入出差地点"></n-input>
</div>
</n-descriptions-item>
<n-descriptions-item :span="6">
<template #label>
出差事项
<span v-if="isRequired('reasons')" class="text-red">*</span>
</template>
<div class="">
<n-input class="flex-1" type="textarea" v-model:value="currData.reasons"
placeholder="请输入出差地点"></n-input>
</div>
</n-descriptions-item>
<n-descriptions-item :span="6">
<template #label>
联系方式
<span v-if="isRequired('phone')" class="text-red">*</span>
</template>
<div class="">
<n-input class="flex-1" v-model:value="currData.phone" placeholder="请输入联系方式"></n-input>
</div>
</n-descriptions-item>
<n-descriptions-item :span="6">
<template #label>
备注
<span v-if="isRequired('remarks')" class="text-red">*</span>
</template>
<div class="">
<n-input class="flex-1" type="textarea" v-model:value="currData.remarks" placeholder="请输入备注"></n-input>
</div>
</n-descriptions-item>
</n-descriptions>
</n-spin>
</n-card>
<n-card size="small">
<template #header>
<div class="flex flex-row items-center">
<span>相关附件</span>
</div>
</template>
<n-spin :show="loading">
<n-descriptions label-placement="left" bordered :column="3" label-class="w-120px" size="small">
<n-descriptions-item :span="3" label="附件">
<div>
<NUpload ref="uploadRef" directory-dnd multiple :file-list="fileUploader.fileList.value"
:custom-request="fileUploader.customRequest" show-download-button :default-upload="false" :max="5"
@update:file-list="fileUploader.handleFileUpdate" @finish="fileUploader.handleFileUploadFinish">
<n-button>上传文件</n-button>
</NUpload>
</div>
</n-descriptions-item>
</n-descriptions>
</n-spin>
</n-card>
</n-space>
</div>
</template>
<style scoped>
:deep(.readonly .n-input__input-el) {
cursor: pointer !important;
}
:deep(.readonly .n-input__textarea-el) {
cursor: pointer !important;
}
</style>

View File

@ -1,21 +0,0 @@
<script setup lang="ts">
import {reactive, ref} from 'vue'
import type {VxeGridProps} from 'vxe-table'
import {getColumns, getFormSchema} from "./schema";
import {BasicForm, FormSchema, useForm} from "@/components/Form";
</script>
<template>
<div class="flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto bg-white px-4 py-2">
新增
</div>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,181 @@
<script setup lang="ts">
import { computed, reactive, ref } from "vue";
import { useDialog, useMessage } from "naive-ui";
import { BasicForm, useForm } from "@/components/Form";
import { mergeGridProps } from "@/utils/vxeTable";
import DetailModal from "@/components/DetailModal/src/DetailModal.vue";
import TripEditModal from "@/views/bussiness-trip/home/TripEditModal.vue";
import TripAuditModal from "@/views/bussiness-trip/home/TripAuditModal.vue";
import { DutyApi } from "@/api/office/duty";
import { PrimaryKey, getColumns, getFormSchema } from "./schema";
import { getTripList } from "@/api/office/bussiness-trip";
import { useRouterPush } from "@/hooks/common/router";
import { paginationProps } from "@/utils/alova";
import { usePagination } from 'alova/client';
import { useVxeTable } from '@/hooks/common/vxeTable';
const { xGridRef, gridProps, triggerProxy } = useVxeTable({ ref: 'xGridRef' });
const { routerPushByKey } = useRouterPush();
const message = useMessage();
const dialog = useDialog();
const searchParams = reactive<any>({});
const detailModalRef = ref(null)
const editModalRef = ref(null);
const auditModalRef = ref(null);
const tabKey = ref<'all' | 'todo'>('all')
/** Hooks - 表单 */
const [register, { setFieldsValue, getFieldsValue }] = useForm({
gridProps: { cols: "1 s:2 m:3 l:3 xl:4 2xl:4" },
labelWidth: 80,
schemas: getFormSchema(),
});
/** Hooks - 数据请求 查询待办 */
const { send, onSuccess } = usePagination(({ pageNum, pageSize }: any) =>
Apis.common.post_workflow_querymytodotask({ params: { pageNum, pageSize } }),
{ ...paginationProps(), immediate: false }
);
onSuccess((res) => {
console.log(res);
});
/** Hooks - 数据请求 查询待办和已办 */
const { send: doneSend, onSuccess: onDoneSuccess } = usePagination(({ pageNum, pageSize }: any) =>
Apis.common.post_workflow_gethistorictaskbyuserid({ params: { pageNum, pageSize } }),
{ ...paginationProps(), immediate: false }
);
onDoneSuccess((res) => {
console.log(res);
});
/** Hooks - 表格 待办 */
const gridOptions = reactive(gridProps({
columns: getColumns(),
proxyConfig: {
autoLoad: true,
ajax: {
query: ({ page }) => send({ pageNum: page.currentPage, pageSize: page.pageSize })
},
}
}));
/** Hooks - 表格 已办 */
const gridOptions2 = reactive(gridProps({
columns: getColumns(),
proxyConfig: {
autoLoad: true,
ajax: {
query: ({ page }) => doneSend({ pageNum: page.currentPage, pageSize: page.pageSize })
},
}
}));
function openDetailModal(record?: Recordable, isSelect: boolean = false) {
detailModalRef.value?.open({
title: "",
columns: getColumns(),
record: JSON.parse(JSON.stringify(record || {}))
});
}
function openEditModal(record?: Recordable, isSelect: boolean = false) {
editModalRef.value?.open({
isUpdate: Boolean(record && record[PrimaryKey]),
isSelect: isSelect,
record: JSON.parse(JSON.stringify(record || {}))
});
}
function handleDelete(row) {
dialog.warning({
title: "提示",
content: `是否确认删除该条记录?`,
positiveText: "确认",
negativeText: "取消",
onPositiveClick: async () => {
x
await DutyApi.delete({ ids: row[PrimaryKey] });
triggerProxy("reload");
},
});
}
function handleQuery(_values: Recordable) {
triggerProxy("reload");
}
function handleReset(_values: Recordable) {
triggerProxy("reload");
}
/** 选中数据 */
const selectRow: Recordable = computed(() => {
return xGridRef.value?.getRadioRecord() || null
})
/** 单选框选中事件 */
const setSelectRow = (row: Recordable) => {
if (selectRow.value && selectRow.value[PrimaryKey] === row[PrimaryKey]) {
xGridRef.value?.clearRadioRow()
} else {
xGridRef.value?.setRadioRow(row)
}
}
/** 表格单元格单击事件 */
function handleCellClick({ row }) {
setSelectRow(row)
}
function handleTabChange(e) {
tabKey.value = e;
triggerProxy('reload')
}
</script>
<template>
<div class="flex-col-stretch gap-4px overflow-hidden bg-white px-4 py-2 lt-sm:overflow-auto">
<BasicForm @register="register" @submit="handleQuery" @reset="handleReset" />
<div class="vxebasic-table-container">
<VxeGrid ref="xGridRef" v-bind="gridOptions" @cell-click="handleCellClick">
<template #radio_cell="{ row, checked }">
<span class="text-base" @click.stop="setSelectRow(row)">
<icon-mdi-check-circle-outline v-if="checked"></icon-mdi-check-circle-outline>
<icon-mdi-checkbox-blank-circle-outline v-else></icon-mdi-checkbox-blank-circle-outline>
</span>
</template>
<template #baseSlot="{ row }">
<p>开始时间{{ row.starttime }}</p>
<p>结束时间{{ row.endtime }}</p>
</template>
</VxeGrid>
</div>
<DetailModal :columns="getColumns()" :record="selectRow" ref="detailModalRef">
<template #baseSlot="{ row }">
<p>开始时间{{ row.starttime }}</p>
<p>结束时间{{ row.endtime }}</p>
</template>
</DetailModal>
</div>
</template>
<style lang="scss"></style>

View File

@ -18,7 +18,7 @@ import { useVxeTable } from '@/hooks/common/vxeTable';
const { xGridRef, gridProps, triggerProxy } = useVxeTable({ ref: 'xGridRef' });
const { routerPushByKey } = useRouterPush();
const { routerPushByKey, routerPush } = useRouterPush();
const message = useMessage();
const dialog = useDialog();
@ -44,8 +44,6 @@ onSuccess((res) => {
console.log(res);
});
/** Hooks - 数据请求 查询待办和已办 */
/** Hooks - 表格 */
const gridOptions = reactive(gridProps({
@ -67,11 +65,15 @@ function openDetailModal(record?: Recordable, isSelect: boolean = false) {
}
function openEditModal(record?: Recordable, isSelect: boolean = false) {
editModalRef.value?.open({
isUpdate: Boolean(record && record[PrimaryKey]),
isSelect: isSelect,
record: JSON.parse(JSON.stringify(record || {}))
});
if (record && record[PrimaryKey]) {
routerPush({
path: '/bussiness-trip/edit/' + record[PrimaryKey],
})
} else {
routerPush({
path: '/bussiness-trip/edit',
})
}
}
function handleDelete(row) {
@ -89,12 +91,6 @@ function handleDelete(row) {
}
function openAuditModal() {
auditModalRef.value?.open({
});
}
function handleQuery(_values: Recordable) {
triggerProxy("reload");
}
@ -128,6 +124,7 @@ function handleCellClick({ row }) {
<template>
<div class="flex-col-stretch gap-4px overflow-hidden bg-white px-4 py-2 lt-sm:overflow-auto">
<BasicForm @register="register" @submit="handleQuery" @reset="handleReset" />
<div class="vxebasic-table-container">
@ -162,15 +159,6 @@ function handleCellClick({ row }) {
删除
</NButton>
<n-badge :value="2" :max="15">
<NButton type="primary" @click="openAuditModal()">
<template #icon>
<icon-mdi-account-check />
</template>
待审批
</NButton>
</n-badge>
</NSpace>
</template>
@ -202,4 +190,4 @@ function handleCellClick({ row }) {
</template>
<style scoped lang="scss"></style>
<style lang="scss"></style>

View File

@ -0,0 +1,157 @@
<script setup lang="ts">
import { computed, reactive, ref } from "vue";
import { useDialog, useMessage } from "naive-ui";
import { BasicForm, useForm } from "@/components/Form";
import { mergeGridProps } from "@/utils/vxeTable";
import DetailModal from "@/components/DetailModal/src/DetailModal.vue";
import TripEditModal from "@/views/bussiness-trip/home/TripEditModal.vue";
import TripAuditModal from "@/views/bussiness-trip/home/TripAuditModal.vue";
import { DutyApi } from "@/api/office/duty";
import { PrimaryKey, getColumns, getFormSchema } from "./schema";
import { getTripList } from "@/api/office/bussiness-trip";
import { useRouterPush } from "@/hooks/common/router";
import { paginationProps } from "@/utils/alova";
import { usePagination } from 'alova/client';
import { useVxeTable } from '@/hooks/common/vxeTable';
const { xGridRef, gridProps, triggerProxy } = useVxeTable({ ref: 'xGridRef' });
const { routerPushByKey } = useRouterPush();
const message = useMessage();
const dialog = useDialog();
const searchParams = reactive<any>({});
const detailModalRef = ref(null)
const editModalRef = ref(null);
const auditModalRef = ref(null);
const tabKey = ref<'all' | 'todo'>('all')
/** Hooks - 数据请求 查询待办 */
const { send, onSuccess } = usePagination(({ pageNum, pageSize }: any) =>
Apis.common.post_workflow_querymytodotask({ params: { pageNum, pageSize } }),
{ ...paginationProps(), immediate: false }
);
onSuccess((res) => {
console.log(res);
});
/** Hooks - 数据请求 查询待办和已办 */
const { send: doneSend, onSuccess: onDoneSuccess } = usePagination(({ pageNum, pageSize }: any) =>
Apis.common.post_workflow_gethistorictaskbyuserid({ params: { pageNum, pageSize } }),
{ ...paginationProps(), immediate: false }
);
onDoneSuccess((res) => {
console.log(res);
});
/** Hooks - 表格 待办 */
const gridOptions = reactive(gridProps({
columns: getColumns(),
proxyConfig: {
autoLoad: true,
ajax: {
query: ({ page }) => send({ pageNum: page.currentPage, pageSize: page.pageSize })
},
},
toolbarConfig:{
enabled:false
}
}));
/** Hooks - 表格 已办 */
const gridOptions2 = reactive(gridProps({
columns: getColumns(),
proxyConfig: {
autoLoad: true,
ajax: {
query: ({ page }) => doneSend({ pageNum: page.currentPage, pageSize: page.pageSize })
},
},
toolbarConfig:{
enabled:false
}
}));
function openEditModal(record?: Recordable, isSelect: boolean = false) {
editModalRef.value?.open({
isUpdate: Boolean(record && record[PrimaryKey]),
isSelect: isSelect,
record: JSON.parse(JSON.stringify(record || {}))
});
}
/** 选中数据 */
const selectRow: Recordable = computed(() => {
return xGridRef.value?.getRadioRecord() || null
})
/** 单选框选中事件 */
const setSelectRow = (row: Recordable) => {
if (selectRow.value && selectRow.value[PrimaryKey] === row[PrimaryKey]) {
xGridRef.value?.clearRadioRow()
} else {
xGridRef.value?.setRadioRow(row)
}
}
/** 表格单元格单击事件 */
function handleCellClick({ row }) {
setSelectRow(row)
}
</script>
<template>
<div class="flex-col-stretch gap-4px overflow-hidden bg-white px-4 py-2 lt-sm:overflow-auto">
<NSpace vertical>
<NCard size="small" title="待办工作" class="flex lex-col">
<div class="vxebasic-table-container">
<VxeGrid ref="xGridRef" v-bind="gridOptions" @cell-click="handleCellClick">
<template #radio_cell="{ row, checked }">
<span class="text-base" @click.stop="setSelectRow(row)">
<icon-mdi-check-circle-outline v-if="checked"></icon-mdi-check-circle-outline>
<icon-mdi-checkbox-blank-circle-outline v-else></icon-mdi-checkbox-blank-circle-outline>
</span>
</template>
<template #baseSlot="{ row }">
<p>开始时间{{ row.starttime }}</p>
<p>结束时间{{ row.endtime }}</p>
</template>
</VxeGrid>
</div>
</NCard>
<NCard size="small" title="已办工作" class="flex lex-col">
<div class="vxebasic-table-container">
<VxeGrid ref="xGridRef" v-bind="gridOptions" @cell-click="handleCellClick">
<template #radio_cell="{ row, checked }">
<span class="text-base" @click.stop="setSelectRow(row)">
<icon-mdi-check-circle-outline v-if="checked"></icon-mdi-check-circle-outline>
<icon-mdi-checkbox-blank-circle-outline v-else></icon-mdi-checkbox-blank-circle-outline>
</span>
</template>
<template #baseSlot="{ row }">
<p>开始时间{{ row.starttime }}</p>
<p>结束时间{{ row.endtime }}</p>
</template>
</VxeGrid>
</div>
</NCard>
</NSpace>
</div>
</template>
<style lang="scss"></style>

View File

@ -0,0 +1,39 @@
import type { VxeGridPropTypes } from 'vxe-table';
import type { FormSchema } from '@/components/Form';
import { useRender } from '@/components/Table';
export const PrimaryKey = 'guid';
export function getColumns(_params: any = {}): VxeGridPropTypes.Columns {
return [
{
field: 'applyTime',
title: '申请日期',
width: 130,
fixed: 'left',
},
{ field: 'base', title: '出差时间', width: 200, slots: { default: 'baseSlot' } },
{ field: 'days', title: '出差天数', width: 80 },
{ field: 'place', title: '出差地点', width: 200, },
{ field: 'reasons', title: '出差事项', width: 200 },
{ field: 'phone', title: '联系方式', width: 200 },
// { field: 'place', title: '出差地点', width: 200, slots: { default: 'ddy' } },
{ field: 'remarks', title: '备注', minWidth: 200 },
];
}
export function getFormSchema(_params: any = {}): FormSchema[] {
return [
{
field: 'date',
component: 'NDatePicker',
label: '填报日期',
componentProps: {
type: 'daterange',
closeOnSelect: true,
placeholder: '',
valueFormat: 'yyyy-MM-dd'
}
}
];
}

View File

@ -0,0 +1,361 @@
<script setup lang="ts">
import { reactive, ref, computed, onMounted } from "vue";
import { useDialog, useMessage } from "naive-ui";
import type { UploadCustomRequestOptions, UploadFileInfo, UploadInst } from "naive-ui";
import {
getMeetingList,
getAddressorList,
saveMeeting,
} from "@/api/office/meeting";
import { useVxeTable } from "@/hooks/common/vxeTable";
import { useRoute } from "vue-router";
import { useRequest } from "alova/client";
import { DICT_TYPE, getDictDefaultObj, getDictObj, getDictOptions } from "@/utils/dict";
import { uploadFile, uploadFiles, downloadFile } from "@/api/system/file";
import ChooseUserModal from "@/views/user-center/ChooseUserModal.vue";
import { router } from "@/router";
import { useTabStore } from "@/store/modules/tab";
import { FileUploader } from "@/utils/file";
import { FileSource } from "@/enums";
import { useUserStore } from "@/store/modules/user";
import dayjs from "dayjs";
import { useRouterPush } from "@/hooks/common/router";
const { routerPushByKey, routerPush } = useRouterPush();
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
const { xGridRef, gridProps, triggerProxy } = useVxeTable({ ref: 'xGridRef' });
const PrimaryKey = "guid"
const message = useMessage();
const dialog = useDialog();
const route = useRoute();
const id = route.params.id;
const tabStore = useTabStore();
const chooseUserModalRef = ref();
const spokespersonModalRef = ref();
const selectField = ref("");
const uploadRef = ref<UploadInst | null>(null);
const fileUploader = new FileUploader({
uploadRef,
data: { source: FileSource.Erp },
onSuccess: (fileList: any[]) => {
console.log('[ fileList ] >', fileList)
handleSubmit('submit')
},
onError: (err: any) => {
console.log('[ err ] >', err)
},
})
/** 必填字段校验 */
const requiredFieldRules: any = {
"timeRanger": { required: true, message: '请选择出差时间' },
"days": { required: true, message: '请输入出差天数' },
"place": { required: true, message: '请输入出差地点' },
}
/** 必填字段校验,如果是必填字段则返回对应message */
function isRequired(field: string): string {
let rule = requiredFieldRules[field];
if (rule && rule.required) {
return rule.message || `${field}不可为空`;
}
return ""
}
/** Hooks 数据请求 - 会议 */
const currData = ref<any>({
phone: userInfo.value.phone
});
const { send, onSuccess, loading } = useRequest(Apis.erp.get_ccsq_page({ params: { guid: id } }), {
immediate: false,
});
onSuccess((res: any) => {
console.log(res);
if (res.data && res.data?.rows && res.data?.rows.length > 0) {
currData.value = res.data?.rows[0] || {};
try {
currData.value.timeRanger = [dayjs(currData.value.starttime).format('YYYY-MM-DD hh:mm'), dayjs(currData.value.endtime).format('YYYY-MM-DD hh:mm')];
} catch (err) {
console.log(err)
}
//
if (currData.value?.fileUuid) {
Apis.common.get_attachment_list({
params: {
uuid: currData.value?.fileUuid,
},
}).then((res: any) => {
console.log(res);
fileUploader.setFileList(res.rows);
console.log(fileUploader.fileList.value)
});
}
console.log("当前数据:", currData.value);
} else {
dialog.error({
title: '提示',
content: '当出差记录不存在',
positiveText: '返回上一层',
closable: false,
maskClosable: false,
onPositiveClick: () => {
back()
}
})
}
});
/**
* 提交
*/
async function handleSubmit(type: "submit" | "upload") {
//
for (const field of Object.keys(requiredFieldRules)) {
if (!currData.value[field]) {
message.error(isRequired(field));
return
}
}
// let loading = message.loading("");
console.log(currData.value);
try {
if (type === "submit") {
//
if (fileUploader.fileList.value && fileUploader.fileList.value.length) {
let fileUuids = fileUploader.fileList.value.map((item) => item.batchId);
currData.value.fileUuid = fileUuids.join(',')
} else {
currData.value.fileUuid = ''
}
let values = JSON.parse(JSON.stringify(currData.value))
values.starttime = values.timeRanger?.[0];
values.endtime = values.timeRanger?.[1];
values.timeRanger = undefined;
values.applyName = userStore.userInfo.gwmc;
values.applyId = userStore.userInfo.userId;
values.applyTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
await Apis.erp.post_ccsq_save({
data: values
});
message.success("提交成功");
back();
} else {
fileUploader.initiateUpload();
}
} catch (error) {
console.error(error)
} finally {
// loading.destroy()
}
}
/**
* 页面返回并关闭tab
*/
function back() {
router.removeRoute("bussiness-trip_home")
tabStore.removeActiveTab();
}
onMounted(async () => {
if (id) {
await send();
}
});
const containerRef = ref<HTMLElement | undefined>(undefined);
function calculateDaysBetweenTimestamps(timestamp1, timestamp2) {
const date1 = new Date(timestamp1);
const date2 = new Date(timestamp2);
const timeDifference = Math.abs(date2 - date1);
const daysDifference = timeDifference / (24 * 60 * 60 * 1000);
const roundedDaysDifference = Math.round(daysDifference * 2) / 2;
return roundedDaysDifference;
}
function handleUpdateFormattedValue(value) {
const startTimestamp = new Date(value[0]).getTime();
const endTimestamp = new Date(value[1]).getTime();
const daysBetween = calculateDaysBetweenTimestamps(startTimestamp, endTimestamp);
currData.value.days = daysBetween;
}
function handleBack() {
dialog.warning({
title: "提示",
content: `是否确认返回上一页面?`,
positiveText: "确认",
negativeText: "取消",
onPositiveClick: async () => {
back();
},
});
}
</script>
<template>
<div ref="containerRef"
class="flex-col-stretch relative gap-16px overflow-hidden bg-white px-4 py-2 lt-sm:overflow-auto">
<div>
<n-affix class="z-20 w-full bg-white py-1" :trigger-top="10" position="absolute" :listen-to="() => containerRef">
<n-space>
<n-button type="primary" size="small" @click="handleSubmit('upload')">
<template #icon>
<icon-mdi-send />
</template>
提交
</n-button>
<n-button size="small" @click="handleBack()">
<template #icon>
<icon-mdi-undo-variant />
</template>
返回
</n-button>
</n-space>
</n-affix>
</div>
<n-space vertical class="overflow-auto">
<div class="h-20px"></div>
<n-card size="small">
<template #header>
<div class="flex flex-row items-center">
<span>基本信息</span>
</div>
</template>
<n-spin :show="loading">
<n-descriptions label-placement="left" bordered :column="6" label-class="w-120px!" size="small">
<n-descriptions-item :span="6">
<template #label>
出差时间
<span v-if="isRequired('timeRanger')" class="text-red">*</span>
</template>
<NDatePicker v-model:formatted-value="currData.timeRanger" type="datetimerange" :close-on-select="true"
placeholder="请选择出差时间" format="yyyy-MM-dd HH:mm" valueFormat="yyyy-MM-dd HH:mm"
:timePickerProps="{ format: 'hh:mm' }" @update-formatted-value="handleUpdateFormattedValue">
</NDatePicker>
</n-descriptions-item>
<n-descriptions-item :span="6">
<template #label>
天数
<span v-if="isRequired('days')" class="text-red">*</span>
</template>
<div class="">
<n-input-number class="flex-1" v-model:value="currData.days" placeholder="请输入出差天数"></n-input-number>
</div>
</n-descriptions-item>
<n-descriptions-item :span="6">
<template #label>
出差地点
<span v-if="isRequired('place')" class="text-red">*</span>
</template>
<div class="">
<n-input class="flex-1" type="textarea" v-model:value="currData.place" placeholder="请输入出差地点"></n-input>
</div>
</n-descriptions-item>
<n-descriptions-item :span="6">
<template #label>
出差事项
<span v-if="isRequired('reasons')" class="text-red">*</span>
</template>
<div class="">
<n-input class="flex-1" type="textarea" v-model:value="currData.reasons"
placeholder="请输入出差地点"></n-input>
</div>
</n-descriptions-item>
<n-descriptions-item :span="6">
<template #label>
联系方式
<span v-if="isRequired('phone')" class="text-red">*</span>
</template>
<div class="">
<n-input class="flex-1" v-model:value="currData.phone" placeholder="请输入联系方式"></n-input>
</div>
</n-descriptions-item>
<n-descriptions-item :span="6">
<template #label>
备注
<span v-if="isRequired('remarks')" class="text-red">*</span>
</template>
<div class="">
<n-input class="flex-1" type="textarea" v-model:value="currData.remarks" placeholder="请输入备注"></n-input>
</div>
</n-descriptions-item>
</n-descriptions>
</n-spin>
</n-card>
<n-card size="small">
<template #header>
<div class="flex flex-row items-center">
<span>相关附件</span>
</div>
</template>
<n-spin :show="loading">
<n-descriptions label-placement="left" bordered :column="3" label-class="w-120px" size="small">
<n-descriptions-item :span="3" label="附件">
<div>
<NUpload ref="uploadRef" directory-dnd multiple :file-list="fileUploader.fileList.value"
:custom-request="fileUploader.customRequest" show-download-button :default-upload="false" :max="5"
@update:file-list="fileUploader.handleFileUpdate" @finish="fileUploader.handleFileUploadFinish">
<n-button>上传文件</n-button>
</NUpload>
</div>
</n-descriptions-item>
</n-descriptions>
</n-spin>
</n-card>
</n-space>
</div>
</template>
<style scoped>
:deep(.readonly .n-input__input-el) {
cursor: pointer !important;
}
:deep(.readonly .n-input__textarea-el) {
cursor: pointer !important;
}
</style>

View File

@ -2,7 +2,6 @@ import process from 'node:process'
import { URL, fileURLToPath } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import dayjs from 'dayjs'
import { VxeUI } from 'vxe-pc-ui'
import { setupVitePlugins } from './build/plugins'
import { createViteProxy } from './build/config'