feat(workflow): 集成自动化审批功能并优化多个业务模块
- 在认证存储中加载审计策略并重置时清空自动审批状态
- 在请假创建页面添加自动审批条件判断和初始化逻辑
- 将多个模块的时间格式化从日期时间改为仅日期格式
- 在CRM合同管理中添加复选框列和操作栏并移除公海天数字段
- 更新MES计量检测和来料检验的数据结构移除用户选择器
- 在物料管理中添加规格型号搜索功能
- 优化描述组件和表单适配器以支持自动审批模式隐藏字段
- 更新客户分配表单以支持批量操作
- 在工作台反馈历史标签页控制审批按钮显示
- 在BPM模型页面同步全局自动审批状态
- 优化CRM待办事项页面适配自动审批模式
- 在合同和发票提交时根据自动审批状态显示不同提示
- 扩展能耗分析模块添加导出和告警功能
- 在HRM模块的离职和调岗流程中指定流程定义键
- 更新API接口支持检测人姓名和供应商名称字段
| | |
| | | import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; |
| | | import { $t } from '@vben/locales'; |
| | | import { isMobile } from '@vben/utils'; |
| | | import { watch } from 'vue'; |
| | | |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import { isAuditField } from '#/utils/audit-ui-policy'; |
| | | |
| | | async function initSetupVbenForm() { |
| | | setupVbenForm<ComponentType>({ |
| | |
| | | }); |
| | | } |
| | | |
| | | const useVbenForm = useForm<ComponentType, ComponentPropsMap>; |
| | | const useVbenForm = (options: FormProps<ComponentType, ComponentPropsMap>) => { |
| | | const result = useForm<ComponentType, ComponentPropsMap>(options); |
| | | const schema = options.schema; |
| | | if (schema) { |
| | | watch(automaticApproval, () => { |
| | | result[1].setState({ schema: schema.filter((item) => !automaticApproval.value || !isAuditField(item)) }); |
| | | }, { immediate: true }); |
| | | } |
| | | return result; |
| | | }; |
| | | |
| | | export { initSetupVbenForm, useVbenForm, z }; |
| | | |
| | |
| | | |
| | | import type { ComponentPropsMap, ComponentType } from './component'; |
| | | |
| | | import { h } from 'vue'; |
| | | import { h, watch } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | import { $te } from '@vben/locales'; |
| | |
| | | |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | import { $t } from '#/locales'; |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import { isAuditField } from '#/utils/audit-ui-policy'; |
| | | |
| | | import { useVbenForm } from './form'; |
| | | |
| | |
| | | export * from '#/components/table-action'; |
| | | export const useVbenVxeGrid = <T extends Record<string, any>>( |
| | | ...rest: Parameters<typeof useGrid<T, ComponentType, ComponentPropsMap>> |
| | | ) => useGrid<T, ComponentType, ComponentPropsMap>(...rest); |
| | | ) => { |
| | | const options = rest[0]; |
| | | const columns = options.gridOptions?.columns; |
| | | const schema = options.formOptions?.schema; |
| | | const applyPolicy = () => ({ |
| | | ...(columns ? { gridOptions: { columns: columns.filter((item) => !automaticApproval.value || !isAuditField(item)) } } : {}), |
| | | ...(schema ? { formOptions: { schema: schema.filter((item) => !automaticApproval.value || !isAuditField(item)) } } : {}), |
| | | }); |
| | | const result = useGrid<T, ComponentType, ComponentPropsMap>(options); |
| | | watch(automaticApproval, () => result[1].setState(applyPolicy()), { immediate: true }); |
| | | return result; |
| | | }; |
| | | |
| | | export type * from '@vben/plugins/vxe-table'; |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | |
| | | export namespace BpmAutoApproveApi { |
| | | /** æµç¨å
审æ¹é
ç½® */ |
| | |
| | | if (remark) { |
| | | params.append('remark', remark); |
| | | } |
| | | return requestClient.put<boolean>( |
| | | const result = await requestClient.put<boolean>( |
| | | `/bpm/process-auto-approve/update-global?${params.toString()}`, |
| | | ); |
| | | automaticApproval.value = autoApprove; |
| | | return result; |
| | | } |
| | | |
| | | /** ææµç¨å®ä¹ç Key 设置å
审æ¹å¼å
³ï¼ä¸åå¨åæ°å¢ãåå¨åæ´æ°ï¼ */ |
| | |
| | | export function getEnergyAnalysis(params: EnergyFilters & { startDate: string; endDate: string; groupBy: 'DAY' | 'MONTH' }) { |
| | | return requestClient.get<EnergyAnalysis>('/energy/record/analysis', { params }); |
| | | } |
| | | |
| | | export function exportEnergyRecords(params: EnergyFilters) { |
| | | return requestClient.download('/energy/record/export-excel', { params }); |
| | | } |
| | | export function exportEnergyAnalysis(params: EnergyFilters & { startDate: string; endDate: string; groupBy: 'DAY' | 'MONTH' }) { |
| | | return requestClient.download('/energy/record/analysis-export', { params }); |
| | | } |
| | | export type EnergyAlertMetric = 'AMOUNT' | 'QUANTITY' | 'MONTH_AMOUNT' | 'MONTH_QUANTITY'; |
| | | export interface EnergyAlertRule { |
| | | id?: number; |
| | | name: string; |
| | | energyType: EnergyType; |
| | | usageArea?: string; |
| | | productCategory?: string; |
| | | metric: EnergyAlertMetric; |
| | | threshold: number; |
| | | enabled: boolean; |
| | | recipientIds: number[]; |
| | | } |
| | | export interface EnergyAlertLog { |
| | | id: number; ruleName: string; recordDate: string; energyType: EnergyType; |
| | | scope: string; metric: EnergyAlertMetric; periodType: 'DAY' | 'MONTH'; actualValue: number; threshold: number; |
| | | recipientName: string; status: 'FAILED' | 'SENT'; errorMessage: string; createTime: string; |
| | | } |
| | | export async function getEnergyAlertRules(params: { pageNo: number; pageSize: number }) { |
| | | const result = await requestClient.get<PageResult<Omit<EnergyAlertRule, 'recipientIds'> & { recipientIds: string }>>('/energy/alert/page', { params }); |
| | | return { ...result, list: result.list.map(row => ({ ...row, recipientIds: row.recipientIds.split(',').filter(Boolean).map(Number) })) }; |
| | | } |
| | | export function getEnergyAlertLogs(params: { pageNo: number; pageSize: number; date?: string; status?: string }) { |
| | | return requestClient.get<PageResult<EnergyAlertLog>>('/energy/alert/logs', { params }); |
| | | } |
| | | export function getEnergyAlertRecipients() { |
| | | return requestClient.get<{ id: number; nickname: string }[]>('/energy/alert/recipients'); |
| | | } |
| | | export function saveEnergyAlertRule(data: EnergyAlertRule) { |
| | | return data.id ? requestClient.put('/energy/alert/update', data) : requestClient.post('/energy/alert/create', data); |
| | | } |
| | | export function deleteEnergyAlertRule(id: number) { return requestClient.delete('/energy/alert/delete', { params: { id } }); } |
| | | export function checkEnergyAlerts(date: string) { return requestClient.post<{ sent: number; failed: number }>('/energy/alert/check', undefined, { params: { date } }); } |
| | |
| | | |
| | | /** æäº¤ç¦»èç³è¯·ï¼å起审æ¹ï¼ */ |
| | | export function submitResignation(id: number) { |
| | | return requestClient.post(`/hrm/resignation/submit?id=${id}`); |
| | | return requestClient.post(`/hrm/resignation/submit?id=${id}&processDefinitionKey=hrm_resignation_approve`); |
| | | } |
| | | |
| | | /** æ¤é离èç³è¯· */ |
| | |
| | | |
| | | /** æäº¤è°å²ç³è¯·ï¼å起审æ¹ï¼ */ |
| | | export function submitTransfer(id: number) { |
| | | return requestClient.post(`/hrm/transfer/submit?id=${id}`); |
| | | return requestClient.post(`/hrm/transfer/submit?id=${id}&processDefinitionKey=hrm_transfer_approve`); |
| | | } |
| | | |
| | | /** æ¤éè°å²ç³è¯· */ |
| | |
| | | checkResult?: number; // æ£æµç»è®ºï¼åå
¸ï¼ |
| | | certNo?: string; // è¯ä¹¦ç¼å· |
| | | checkUserId?: number; // æ£æµäººç¼å· |
| | | checkUserName?: string; // æå·¥å¡«åçæ£æµäººå§å |
| | | checkUserNickname?: string; // æ£æµäººæµç§° |
| | | nextCheckDate?: string; // 䏿¬¡æ£æµæ¥æ |
| | | remark?: string; // 夿³¨ |
| | |
| | | sourceLineId?: number; // æ¥æºåæ®è¡ ID |
| | | sourceDocCode?: string; // æ¥æºåæ®ç¼å·ï¼å
³èæ¥è¯¢ï¼ |
| | | vendorId?: number; // ä¾åºå ID |
| | | vendorName?: string; // ä¾åºååç§°ï¼æå¡«ï¼ |
| | | vendorNickname?: string; // ä¾åºåç®ç§°ï¼å
³èæ¥è¯¢ï¼ |
| | | vendorBatch?: string; // ä¾åºåæ¹æ¬¡å· |
| | | itemId?: number; // 产åç©æ ID |
| | |
| | | |
| | | import { Card, Descriptions } from 'ant-design-vue'; |
| | | |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import { isAuditField } from '#/utils/audit-ui-policy'; |
| | | |
| | | const props = { |
| | | bordered: { default: true, type: Boolean }, |
| | | column: { |
| | |
| | | const { data, schema } = unref(getProps); |
| | | return unref(schema) |
| | | .map((item) => { |
| | | if (automaticApproval.value && isAuditField(item)) return null; |
| | | const { contentMinWidth, field, render, show, span } = item; |
| | | |
| | | if (show && isFunction(show) && !show(data)) { |
| | |
| | | Tooltip, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import { auditActionMode } from '#/utils/audit-ui-policy'; |
| | | |
| | | const props = defineProps({ |
| | | actions: { |
| | | type: Array as PropType<ActionItem[]>, |
| | |
| | | |
| | | /** æ£æ¥æ¯å¦æ¾ç¤º */ |
| | | function isIfShow(action: ActionItem): boolean { |
| | | if (auditActionMode(action.label, automaticApproval.value) === 'hidden') return false; |
| | | const ifShow = action.ifShow; |
| | | let isIfShow = true; |
| | | if (isBoolean(ifShow)) { |
| | |
| | | /** å¤çæé® actions */ |
| | | const getActions = computed(() => { |
| | | const actions = props.actions || []; |
| | | return actions.filter((action: ActionItem) => isIfShow(action)); |
| | | return actions.filter((action: ActionItem) => isIfShow(action)).map(displayAction); |
| | | }); |
| | | |
| | | /** å¤ç䏿èå actions */ |
| | | const getDropdownList = computed(() => { |
| | | const dropDownActions = props.dropDownActions || []; |
| | | return dropDownActions.filter((action: ActionItem) => isIfShow(action)); |
| | | return dropDownActions.filter((action: ActionItem) => isIfShow(action)).map(displayAction); |
| | | }); |
| | | |
| | | function displayAction(action: ActionItem): ActionItem { |
| | | return auditActionMode(action.label, automaticApproval.value) === 'activate' |
| | | ? { ...action, label: 'çæ', tooltip: 'æäº¤åèªå¨çæ' } |
| | | : action; |
| | | } |
| | | |
| | | /** Space ç»ä»¶ç size */ |
| | | const spaceSize = computed(() => { |
| | | const actions = unref(getActions); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export const automaticApproval = ref(false); |
| | | |
| | | /** Loaded with each authenticated session, before business pages are rendered. */ |
| | | export async function loadAuditPolicy() { |
| | | try { |
| | | automaticApproval.value = await requestClient.get<boolean>('/bpm/process-auto-approve/ui-policy'); |
| | | } catch { |
| | | // A failed policy request must not remove the normal submission path. |
| | | automaticApproval.value = false; |
| | | } |
| | | } |
| | |
| | | socialLogin, |
| | | } from '#/api'; |
| | | import { $t } from '#/locales'; |
| | | import { automaticApproval, loadAuditPolicy } from '#/store/audit-policy'; |
| | | |
| | | export const useAuthStore = defineStore('auth', () => { |
| | | const accessStore = useAccessStore(); |
| | |
| | | // eslint-disable-next-line no-useless-assignment |
| | | let authPermissionInfo: AuthPermissionInfo | null = null; |
| | | authPermissionInfo = await getAuthPermissionInfoApi(); |
| | | await loadAuditPolicy(); |
| | | // userStore |
| | | userStore.setUserInfo(authPermissionInfo.user); |
| | | userStore.setUserRoles(authPermissionInfo.roles); |
| | |
| | | |
| | | function $reset() { |
| | | loginLoading.value = false; |
| | | automaticApproval.value = false; |
| | | } |
| | | |
| | | return { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** Review-only UI disappears in automatic mode; document status and activation remain. */ |
| | | export function isAuditField(item: { field?: string; fieldName?: string; label?: unknown; title?: unknown }) { |
| | | return /^(auditStatus|auditUserId|auditTime|approvalStatus|approvalUserId|approvalTime|processInstanceId)$/.test(item.field ?? item.fieldName ?? '') |
| | | || /^(审æ¹ç¶æ|å®¡æ ¸ç¶æ|审æ¹äºº|å®¡æ ¸äºº|å®¡æ¹æ¶é´|å®¡æ ¸æ¶é´|æµç¨å®ä¾)$/.test(String(item.title ?? item.label ?? '')); |
| | | } |
| | | |
| | | export function auditActionMode(label: string | undefined, automatic: boolean) { |
| | | if (!automatic) return 'normal'; |
| | | if (/^(æäº¤å®¡æ¹|æäº¤å®¡æ ¸|å®¡æ ¸|审æ¹|éè¿|å®¡æ ¸éè¿)$/.test(label ?? '')) return 'activate'; |
| | | if (/^(æ¥ç审æ¹|æ¥çå®¡æ ¸|审æ¹è¿åº¦|æ¥ç审æ¹è¿åº¦|åå®¡æ ¸|æ¤é审æ¹|忶审æ¹|驳å|æç»)$/.test(label ?? '')) return 'hidden'; |
| | | return 'normal'; |
| | | } |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import type { ModelCategoryInfo } from '#/api/bpm/model'; |
| | | |
| | | import { |
| | |
| | | globalAutoApproveLoading.value = true; |
| | | try { |
| | | await updateGlobalAutoApprove(checked); |
| | | automaticApproval.value = checked; |
| | | message.success( |
| | | checked |
| | | ? 'å·²å¼å¯å
¨å±å
审æ¹ï¼æææµç¨æäº¤åå°ç´æ¥éè¿' |
| | |
| | | import { getApprovalDetail as getApprovalDetailApi } from '#/api/bpm/processInstance'; |
| | | import { $t } from '#/locales'; |
| | | import { router } from '#/router'; |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import ProcessInstanceTimeline from '#/views/bpm/processInstance/detail/modules/time-line.vue'; |
| | | |
| | | import { useFormSchema } from './data'; |
| | |
| | | return; |
| | | } |
| | | // 1.2 审æ¹ç¸å
³ï¼æ ¡éªæå®å®¡æ¹äºº |
| | | if (startUserSelectTasks.value?.length > 0) { |
| | | if (!automaticApproval.value && startUserSelectTasks.value?.length > 0) { |
| | | for (const userTask of startUserSelectTasks.value) { |
| | | const assignees = startUserSelectAssignees.value[userTask.id]; |
| | | if (Array.isArray(assignees) && assignees.length === 0) { |
| | |
| | | // æäº¤è¡¨å |
| | | const data = (await formApi.getValues()) as LeaveCreateData; |
| | | // 审æ¹ç¸å
³ï¼è®¾ç½®æå®å®¡æ¹äºº |
| | | if (startUserSelectTasks.value?.length > 0) { |
| | | if (!automaticApproval.value && startUserSelectTasks.value?.length > 0) { |
| | | data.startUserSelectAssignees = startUserSelectAssignees.value; |
| | | } |
| | | // æ ¼å¼åå¼å§æ¶é´åç»ææ¶é´çå¼ |
| | |
| | | |
| | | /** 审æ¹ç¸å
³ï¼è·å审æ¹è¯¦æ
*/ |
| | | async function getApprovalDetail() { |
| | | if (automaticApproval.value) return; |
| | | processTimeLineLoading.value = true; |
| | | try { |
| | | const data = await getApprovalDetailApi({ |
| | |
| | | BpmCandidateStrategyEnum.START_USER_SELECT === node.candidateStrategy, |
| | | ); |
| | | // æ¢å¤ä¹åç鿩审æ¹äºº |
| | | if (startUserSelectTasks.value?.length > 0) { |
| | | if (!automaticApproval.value && startUserSelectTasks.value?.length > 0) { |
| | | for (const node of startUserSelectTasks.value) { |
| | | const tempAssignees = tempStartUserSelectAssignees.value[node.id]; |
| | | startUserSelectAssignees.value[node.id] = tempAssignees?.length |
| | |
| | | |
| | | /** åå§å */ |
| | | onMounted(async () => { |
| | | if (automaticApproval.value) { |
| | | if (query.id) await getDetail(Number(query.id)); |
| | | return; |
| | | } |
| | | const processDefinitionDetail: any = await getProcessDefinition( |
| | | undefined, |
| | | processDefineKey, |
| | |
| | | <script lang="ts" setup> |
| | | import { computed, onActivated, onMounted, ref } from 'vue'; |
| | | import { computed, onActivated, onMounted, ref, watch } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | |
| | | import { Badge, Card, List } from 'ant-design-vue'; |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | |
| | | import { getFollowClueCount } from '#/api/crm/clue'; |
| | | import { |
| | |
| | | receivableAuditCount, |
| | | receivablePlanRemindCount, |
| | | ); |
| | | const visibleLeftSides = computed(() => leftSides.filter((item) => |
| | | !automaticApproval.value || !['contractAudit', 'receivableAudit'].includes(item.menu), |
| | | )); |
| | | watch(automaticApproval, (automatic) => { |
| | | if (automatic && ['contractAudit', 'receivableAudit'].includes(leftMenu.value)) { |
| | | leftMenu.value = 'customerTodayContact'; |
| | | } |
| | | }); |
| | | |
| | | const currentComponent = computed(() => { |
| | | const components = { |
| | |
| | | customerPutPoolRemindCount.value = await getPutPoolRemindCustomerCount(); |
| | | customerFollowCount.value = await getFollowCustomerCount(); |
| | | clueFollowCount.value = await getFollowClueCount(); |
| | | contractAuditCount.value = await getAuditContractCount(); |
| | | if (!automaticApproval.value) contractAuditCount.value = await getAuditContractCount(); |
| | | contractRemindCount.value = await getRemindContractCount(); |
| | | receivableAuditCount.value = await getAuditReceivableCount(); |
| | | if (!automaticApproval.value) receivableAuditCount.value = await getAuditReceivableCount(); |
| | | receivablePlanRemindCount.value = await getReceivablePlanRemindCount(); |
| | | } |
| | | |
| | |
| | | <Page auto-content-height> |
| | | <div class="flex h-full w-full"> |
| | | <Card class="w-1/5"> |
| | | <List item-layout="horizontal" :data-source="leftSides"> |
| | | <List item-layout="horizontal" :data-source="visibleLeftSides"> |
| | | <template #renderItem="{ item }"> |
| | | <List.Item |
| | | @click="sideClick(item)" |
| | |
| | | title: 'ååå¼å§æ¶é´', |
| | | field: 'startTime', |
| | | minWidth: 150, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'ååç»ææ¶é´', |
| | | field: 'endTime', |
| | | minWidth: 150, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: '已忬¾éé¢ï¼å
ï¼', |
| | |
| | | title: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | minWidth: 150, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'å建人', |
| | |
| | | title: 'ä¸åæ¶é´', |
| | | field: 'orderDate', |
| | | minWidth: 120, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'ååå¼å§æ¶é´', |
| | | field: 'startTime', |
| | | minWidth: 120, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'ååç»ææ¶é´', |
| | | field: 'endTime', |
| | | minWidth: 120, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: '客æ·ç¾çº¦äºº', |
| | |
| | | title: 'æåè·è¿æ¶é´', |
| | | field: 'contactLastTime', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'è´è´£äºº', |
| | |
| | | title: 'æ´æ°æ¶é´', |
| | | field: 'updateTime', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'å建人', |
| | |
| | | import { |
| | | erpCountInputFormatter, |
| | | erpPriceInputFormatter, |
| | | formatDateTime, |
| | | formatDate, |
| | | } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | |
| | | { |
| | | label: 'ä¸åæ¶é´', |
| | | field: 'orderDate', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | label: 'ååå¼å§æ¶é´', |
| | | field: 'startTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | label: 'ååç»ææ¶é´', |
| | | field: 'endTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { label: '客æ·ç¾çº¦äºº', field: 'signContactName', render: textRender }, |
| | | { label: 'å
¬å¸ç¾çº¦äºº', field: 'signUserName', render: textRender }, |
| | |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | label: 'ååç¶æ', |
| | |
| | | import { h } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; |
| | | import { erpPriceInputFormatter, formatDate } from '@vben/utils'; |
| | | |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | |
| | |
| | | { |
| | | field: 'orderDate', |
| | | label: 'ä¸åæ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'totalReceivablePrice', |
| | |
| | | { |
| | | field: 'orderDate', |
| | | label: 'ä¸åæ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'startTime', |
| | | label: 'ååå¼å§æ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'endTime', |
| | | label: 'ååç»ææ¶é´', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | field: 'signContactName', |
| | |
| | | submitContract, |
| | | } from '#/api/crm/contract'; |
| | | import { $t } from '#/locales'; |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | |
| | | const submitLoading = ref(false); |
| | | |
| | | async function handleSubmit(row: CrmContractApi.Contract) { |
| | | const res = await getContractApproveProcessList(); |
| | | const res = automaticApproval.value ? [] : await getContractApproveProcessList(); |
| | | processList.value = res; |
| | | currentRow.value = row; |
| | | selectedProcessKey.value = undefined; |
| | | if (automaticApproval.value) { |
| | | selectedProcessKey.value = 'contract_approve'; |
| | | await confirmSubmit(); |
| | | return; |
| | | } |
| | | submitModalVisible.value = true; |
| | | } |
| | | |
| | |
| | | submitLoading.value = true; |
| | | try { |
| | | await submitContract(currentRow.value!.id!, selectedProcessKey.value); |
| | | message.success('æäº¤å®¡æ¹æå'); |
| | | message.success(automaticApproval.value ? 'å·²æäº¤çæ' : 'æäº¤å®¡æ¹æå'); |
| | | submitModalVisible.value = false; |
| | | handleRefresh(); |
| | | } finally { |
| | |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | | field: 'poolDay', |
| | | title: 'è·ç¦»è¿å
¥å
¬æµ·å¤©æ°', |
| | | minWidth: 140, |
| | | formatter: ({ cellValue }) => |
| | | cellValue === null ? '-' : `${cellValue} 天`, |
| | | }, |
| | | { |
| | | field: 'ownerUserName', |
| | | title: 'è´è´£äºº', |
| | | minWidth: 100, |
| | |
| | | <script lang="ts" setup> |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | |
| | | import { useDistributeFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const customerIds = ref<number[]>([]); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | |
| | | // æäº¤è¡¨å |
| | | const data = await formApi.getValues(); |
| | | try { |
| | | await distributeCustomer([data.id], data.ownerUserId); |
| | | await distributeCustomer(customerIds.value, data.ownerUserId); |
| | | // å
³éå¹¶æç¤º |
| | | await modalApi.close(); |
| | | emit('success'); |
| | |
| | | return; |
| | | } |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData(); |
| | | if (!data || !data.id) { |
| | | const data = modalApi.getData<{ id?: number; ids?: number[] }>(); |
| | | customerIds.value = data?.ids ?? (data?.id ? [data.id] : []); |
| | | if (customerIds.value.length === 0) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | await formApi.setValues(data); |
| | | await formApi.resetForm(); |
| | | await formApi.setValues({ id: customerIds.value[0] }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { type: 'checkbox', width: 50, fixed: 'left' }, |
| | | { |
| | | title: '客æ·åç§°', |
| | | field: 'name', |
| | |
| | | field: 'creatorName', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 150, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { CrmCustomerApi } from '#/api/crm/customer'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { confirm, Page, useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | | import { Button, message } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { exportCustomer, getCustomerPage } from '#/api/crm/customer'; |
| | | import { |
| | | exportCustomer, |
| | | getCustomerPage, |
| | | receiveCustomer, |
| | | } from '#/api/crm/customer'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import DistributeForm from '../detail/modules/distribute-form.vue'; |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | |
| | | const { push } = useRouter(); |
| | | const receiving = ref(false); |
| | | const [DistributeModal, distributeModalApi] = useVbenModal({ |
| | | connectedComponent: DistributeForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | function handleRefresh() { |
| | | gridApi.grid.clearCheckboxRow(); |
| | | gridApi.query(); |
| | | } |
| | | |
| | | function getCustomerIds(row?: CrmCustomerApi.Customer): number[] { |
| | | const records: CrmCustomerApi.Customer[] = row |
| | | ? [row] |
| | | : gridApi.grid.getCheckboxRecords(); |
| | | const ids = records.map((item) => item.id!); |
| | | if (ids.length === 0) { |
| | | message.warning('请å
å¾é客æ·'); |
| | | } |
| | | return ids; |
| | | } |
| | | |
| | | /** é¢ååæ¡æå¾éçå
¬æµ·å®¢æ· */ |
| | | async function handleReceive(row?: CrmCustomerApi.Customer) { |
| | | if (receiving.value) return; |
| | | const ids = getCustomerIds(row); |
| | | if (ids.length === 0) return; |
| | | receiving.value = true; |
| | | try { |
| | | try { |
| | | await confirm( |
| | | row |
| | | ? `ç¡®å®é¢å客æ·ã${row.name}ãåï¼` |
| | | : `ç¡®å®é¢åéä¸ç ${ids.length} 个客æ·åï¼`, |
| | | ); |
| | | } catch { |
| | | return; |
| | | } |
| | | await receiveCustomer(ids); |
| | | message.success('é¢åå®¢æ·æå'); |
| | | handleRefresh(); |
| | | } finally { |
| | | receiving.value = false; |
| | | } |
| | | } |
| | | |
| | | /** åé
åæ¡æå¾éçå
¬æµ·å®¢æ· */ |
| | | function handleDistribute(row?: CrmCustomerApi.Customer) { |
| | | const ids = getCustomerIds(row); |
| | | if (ids.length === 0) return; |
| | | distributeModalApi.setData({ ids }).open(); |
| | | } |
| | | |
| | | /** 导åºè¡¨æ ¼ */ |
| | | async function handleExport() { |
| | | const data = await exportCustomer(await gridApi.formApi.getValues()); |
| | | const data = await exportCustomer({ |
| | | ...(await gridApi.formApi.getValues()), |
| | | pool: true, |
| | | sceneType: 4, |
| | | }); |
| | | downloadFileFromBlobPart({ fileName: '客æ·å
¬æµ·.xls', source: data }); |
| | | } |
| | | |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height><Grid> |
| | | <Page auto-content-height> |
| | | <DistributeModal @success="handleRefresh" /> |
| | | <Grid> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'é¢å客æ·', |
| | | type: 'primary', |
| | | auth: ['crm:customer:receive'], |
| | | disabled: receiving, |
| | | onClick: () => handleReceive(), |
| | | }, |
| | | { |
| | | label: 'åé
客æ·', |
| | | auth: ['crm:customer:distribute'], |
| | | disabled: receiving, |
| | | onClick: () => handleDistribute(), |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'é¢å', |
| | | type: 'link', |
| | | auth: ['crm:customer:receive'], |
| | | disabled: receiving, |
| | | onClick: () => handleReceive(row), |
| | | }, |
| | | { |
| | | label: 'åé
', |
| | | type: 'link', |
| | | auth: ['crm:customer:distribute'], |
| | | disabled: receiving, |
| | | onClick: () => handleDistribute(row), |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #name="{ row }"> |
| | | <Button type="link" @click="handleDetail(row)"> |
| | | {{ row.name }} |
| | |
| | | submitInvoice, |
| | | } from '#/api/crm/invoice'; |
| | | import { $t } from '#/locales'; |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | |
| | | /** æäº¤å®¡æ ¸ */ |
| | | async function handleSubmit(row: CrmInvoiceApi.Invoice) { |
| | | const hideLoading = message.loading({ |
| | | content: 'æäº¤å®¡æ ¸ä¸...', |
| | | content: automaticApproval.value ? 'æ£å¨æäº¤çæ...' : 'æäº¤å®¡æ ¸ä¸...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await submitInvoice(row.id!); |
| | | message.success('æäº¤å®¡æ ¸æå'); |
| | | message.success(automaticApproval.value ? 'å·²æäº¤çæ' : 'æäº¤å®¡æ ¸æå'); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | |
| | | field: 'quotationTime', |
| | | title: 'æ¥ä»·æ¥æ', |
| | | minWidth: 120, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'validUntil', |
| | | title: 'æææè³', |
| | | minWidth: 120, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'status', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 180, |
| | | formatter: 'formatDateTime', |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import type { FormInstance, TablePaginationConfig } from 'ant-design-vue'; |
| | | import type { EnergyAlertRule, EnergyAlertLog, EnergyType } from '#/api/energy'; |
| | | import { computed, onMounted, reactive, ref } from 'vue'; |
| | | import { Page } from '@vben/common-ui'; |
| | | import { Alert, Button, Card, DatePicker, Form, FormItem, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Table, Tabs, TabPane, Tag, message } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | import { checkEnergyAlerts, deleteEnergyAlertRule, getEnergyAlertLogs, getEnergyAlertRecipients, getEnergyAlertRules, saveEnergyAlertRule } from '#/api/energy'; |
| | | import { energyInfo, energyOptions, numberText, productOptions } from '../shared'; |
| | | |
| | | defineOptions({ name: 'EnergyAlert' }); |
| | | const tab = ref('rules'); |
| | | const rules = ref<EnergyAlertRule[]>([]); |
| | | const logs = ref<EnergyAlertLog[]>([]); |
| | | const recipients = ref<{ label: string; value: number }[]>([]); |
| | | const loading = ref(false), error = ref(false), saving = ref(false), checking = ref(false), open = ref(false); |
| | | const rulePage = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true }); |
| | | const logPage = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true }); |
| | | const logFilters = reactive<{ date?: string; status?: string }>({}); |
| | | const checkDate = ref(dayjs().format('YYYY-MM-DD')); |
| | | const initial = (): EnergyAlertRule => ({ name: '', energyType: 'WATER', metric: 'QUANTITY', threshold: 1, enabled: true, recipientIds: [], usageArea: '', productCategory: undefined }); |
| | | const model = reactive<EnergyAlertRule>(initial()); |
| | | const metricOptions = [ |
| | | { label: 'æ¯æ¥ç¨é', value: 'QUANTITY' }, { label: 'æ¯æ¥è´¹ç¨', value: 'AMOUNT' }, |
| | | { label: 'æ¯æç¨é', value: 'MONTH_QUANTITY' }, { label: 'æ¯æè´¹ç¨', value: 'MONTH_AMOUNT' }, |
| | | ]; |
| | | const metricLabel = (metric: string) => metricOptions.find(option => option.value === metric)?.label ?? metric; |
| | | const isAmount = (metric: string) => metric.endsWith('AMOUNT'); |
| | | const formRef = ref<FormInstance>(); |
| | | const unit = computed(() => isAmount(model.metric) ? 'å
' : energyInfo[model.energyType].unit); |
| | | const validation = { |
| | | name: [{ required: true, whitespace: true, message: '请è¾å
¥è§ååç§°' }], |
| | | threshold: [{ required: true, type: 'number' as const, min: 0.001, message: 'éå¼å¿
须大äºé¶' }], |
| | | recipientIds: [{ required: true, type: 'array' as const, min: 1, max: 50, message: 'è¯·éæ© 1 è³ 50 使¥æ¶äºº' }], |
| | | }; |
| | | const ruleColumns = [ |
| | | { title: 'è§ååç§°', dataIndex: 'name', width: 180 }, { title: 'è½æº', dataIndex: 'energyType', width: 80 }, |
| | | { title: 'ç»è®¡èå´', key: 'scope', width: 200 }, { title: 'ç»è®¡ææ / éå¼', key: 'threshold', width: 190 }, |
| | | { title: 'æ¥æ¶äºº', key: 'recipients', width: 180 }, { title: 'ç¶æ', key: 'enabled', width: 80 }, |
| | | { title: 'æä½', key: 'action', width: 140, fixed: 'right' as const }, |
| | | ]; |
| | | const logColumns = [ |
| | | { title: 'ç»è®¡å¨æ', key: 'period', width: 120 }, { title: 'è§ååç§°', dataIndex: 'ruleName', width: 160 }, |
| | | { title: 'è½æº / èå´', key: 'scope', width: 230 }, { title: 'å®é
å¼ / éå¼', key: 'value', width: 210 }, |
| | | { title: 'æ¥æ¶äºº', dataIndex: 'recipientName', width: 120 }, { title: 'ç»æ', key: 'status', width: 100 }, |
| | | { title: '失败åå ', dataIndex: 'errorMessage', width: 300 }, |
| | | ]; |
| | | let loadId = 0; |
| | | async function load() { |
| | | const id = ++loadId; loading.value = true; error.value = false; |
| | | try { |
| | | if (tab.value === 'rules') { |
| | | const result = await getEnergyAlertRules({ pageNo: rulePage.current, pageSize: rulePage.pageSize }); |
| | | if (id === loadId) { rules.value = result.list; rulePage.total = result.total; } |
| | | } else { |
| | | const result = await getEnergyAlertLogs({ ...logFilters, pageNo: logPage.current, pageSize: logPage.pageSize }); |
| | | if (id === loadId) { logs.value = result.list; logPage.total = result.total; } |
| | | } |
| | | } catch { if (id === loadId) error.value = true; } |
| | | finally { if (id === loadId) loading.value = false; } |
| | | } |
| | | function changePage(page: TablePaginationConfig) { |
| | | Object.assign(tab.value === 'rules' ? rulePage : logPage, { current: page.current ?? 1, pageSize: page.pageSize ?? 10 }); void load(); |
| | | } |
| | | async function showForm(row?: EnergyAlertRule) { |
| | | try { recipients.value = (await getEnergyAlertRecipients()).map(user => ({ label: user.nickname, value: user.id })); } |
| | | catch { message.error('æ æ³å è½½æ¥æ¶äººï¼è¯·éè¯'); return; } |
| | | Object.keys(model).forEach(key => delete (model as unknown as Record<string, unknown>)[key]); |
| | | Object.assign(model, row ? { ...row, recipientIds: [...row.recipientIds] } : initial()); |
| | | open.value = true; formRef.value?.clearValidate(); |
| | | } |
| | | async function save() { |
| | | await formRef.value?.validate(); saving.value = true; |
| | | try { await saveEnergyAlertRule({ ...model }); open.value = false; message.success('è§åå·²ä¿åï¼åç»å½å
¥èªå¨æ£æ¥ï¼å岿¥æå¯æå¨æ£æ¥'); await load(); } |
| | | finally { saving.value = false; } |
| | | } |
| | | async function remove(id: number) { await deleteEnergyAlertRule(id); message.success('å·²å é¤ï¼åå²éç¥è®°å½ä¿ç'); rulePage.current = 1; await load(); } |
| | | async function check() { |
| | | if (!checkDate.value || checkDate.value > dayjs().format('YYYY-MM-DD')) { message.warning('è¯·éæ©ä»å¤©æä¹åçæ¥æ'); return; } |
| | | checking.value = true; |
| | | try { |
| | | const result = await checkEnergyAlerts(checkDate.value); |
| | | message[result.failed ? 'warning' : 'success'](`æ£æ¥å®æï¼æ°åé ${result.sent} æ¡ï¼å¤±è´¥ ${result.failed} æ¡ãå·²åéçéç¥ä¸ä¼éå¤åéã`); |
| | | tab.value = 'logs'; logFilters.date = checkDate.value; logPage.current = 1; await load(); |
| | | } finally { checking.value = false; } |
| | | } |
| | | function recipientNames(ids: number[]) { return ids.map(id => recipients.value.find(user => user.value === id)?.label ?? `ç¨æ· ${id}`).join('ã'); } |
| | | onMounted(async () => { |
| | | await load(); |
| | | try { recipients.value = (await getEnergyAlertRecipients()).map(user => ({ label: user.nickname, value: user.id })); } catch { /* Rules remain viewable when recipient lookup fails. */ } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page title="è½èé¢è¦" description="é
ç½®æ¯æ¥ææ¯æç¨éãè´¹ç¨éå¼ï¼è¶
è¿é弿¶åæå®äººååéç«å
éç¥ã"> |
| | | <Alert class="mb-4" type="info" show-icon message="æ¯æ¥ææ æå¤©æ±æ»ï¼æ¯æææ æèªç¶ææ±æ»æ¬æå·²å½å
¥æ°æ®ï¼è¶
éå³éç¥ãåä¸è§åãç»è®¡å¨æåæ¥æ¶äººåªæåéç¥ä¸æ¬¡ãæ£æ¥æ¥æåæ¶æ£æ¥å½å¤©åæå±æä»½ãåºåæå称精确å¹é
ã" /> |
| | | <Card> |
| | | <div class="mb-4 flex flex-wrap items-center justify-between gap-3"> |
| | | <Button v-access:code="['energy:alert:create']" type="primary" @click="showForm()">æ°å¢é¢è¦è§å</Button> |
| | | <Space wrap><span>æ£æ¥æ¥æ</span><DatePicker v-model:value="checkDate" value-format="YYYY-MM-DD" :allow-clear="false" /><Button v-access:code="['energy:alert:check']" :loading="checking" @click="check">æ£æ¥é¢è¦ / éè¯å¤±è´¥éç¥</Button></Space> |
| | | </div> |
| | | <Alert v-if="error" class="mb-4" type="error" show-icon message="å 载失败ï¼è¯·ç¹å»å·æ°éè¯ã" /> |
| | | <Tabs v-model:active-key="tab" @change="load"> |
| | | <TabPane key="rules" tab="é¢è¦è§å"> |
| | | <Table :columns="ruleColumns" :data-source="rules" row-key="id" :loading="loading" :pagination="rulePage" :scroll="{ x: 1100 }" :locale="{ emptyText: 'ææ è§åï¼è¯·å
设置éå¼åéç¥æ¥æ¶äºº' }" @change="changePage"> |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.dataIndex === 'energyType'">{{ energyInfo[record.energyType as EnergyType].name }}</template> |
| | | <template v-else-if="column.key === 'scope'">{{ record.usageArea || 'å
¨é¨åºå' }} / {{ record.productCategory || 'å
¨é¨äº§å' }}</template> |
| | | <template v-else-if="column.key === 'threshold'">{{ metricLabel(record.metric) }} > {{ numberText(record.threshold) }} {{ isAmount(record.metric) ? 'å
' : energyInfo[record.energyType as EnergyType].unit }}</template> |
| | | <template v-else-if="column.key === 'recipients'">{{ recipientNames(record.recipientIds) }}</template> |
| | | <template v-else-if="column.key === 'enabled'"><Tag :color="record.enabled ? 'green' : 'default'">{{ record.enabled ? 'å¯ç¨' : 'åç¨' }}</Tag></template> |
| | | <template v-else-if="column.key === 'action'"><Space :size="0"><Button v-access:code="['energy:alert:update']" type="link" @click="showForm(record as EnergyAlertRule)">ç¼è¾</Button><Popconfirm title="å 餿¤è§åï¼åå²éç¥ä»ä¼ä¿çã" @confirm="remove(record.id)"><Button v-access:code="['energy:alert:delete']" type="link" danger>å é¤</Button></Popconfirm></Space></template> |
| | | </template> |
| | | </Table> |
| | | </TabPane> |
| | | <TabPane key="logs" tab="éç¥è®°å½"> |
| | | <Space class="mb-4" wrap><DatePicker v-model:value="logFilters.date" value-format="YYYY-MM-DD" placeholder="ç»è®¡æ¥æ" /><Select v-model:value="logFilters.status" :options="[{ label: 'åéæå', value: 'SENT' }, { label: 'åé失败', value: 'FAILED' }]" allow-clear placeholder="å
¨é¨ç»æ" style="width: 140px" /><Button @click="logPage.current = 1; load()">æ¥è¯¢</Button></Space> |
| | | <Table :columns="logColumns" :data-source="logs" row-key="id" :loading="loading" :pagination="logPage" :scroll="{ x: 1340 }" :locale="{ emptyText: 'ææ é¢è¦éç¥è®°å½' }" @change="changePage"> |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'period'">{{ record.periodType === 'MONTH' ? record.recordDate.slice(0, 7) : record.recordDate }}<div class="text-xs text-muted-foreground">{{ metricLabel(record.metric) }}</div></template> |
| | | <template v-else-if="column.key === 'scope'">{{ energyInfo[record.energyType as EnergyType].name }} / {{ record.scope }}</template> |
| | | <template v-else-if="column.key === 'value'">{{ numberText(record.actualValue) }} / {{ numberText(record.threshold) }} {{ isAmount(record.metric) ? 'å
' : energyInfo[record.energyType as EnergyType].unit }}</template> |
| | | <template v-else-if="column.key === 'status'"><Tag :color="record.status === 'SENT' ? 'green' : 'red'">{{ record.status === 'SENT' ? 'åéæå' : 'åé失败' }}</Tag></template> |
| | | </template> |
| | | </Table> |
| | | </TabPane> |
| | | <template #rightExtra><Button :loading="loading" @click="load">å·æ°</Button></template> |
| | | </Tabs> |
| | | </Card> |
| | | <Modal v-model:open="open" :title="model.id ? 'ç¼è¾é¢è¦è§å' : 'æ°å¢é¢è¦è§å'" :confirm-loading="saving" :mask-closable="false" :width="640" @ok="save"> |
| | | <Form ref="formRef" :model="model" :rules="validation" layout="vertical" class="mt-4"> |
| | | <FormItem label="è§ååç§°" name="name"><Input v-model:value="model.name" :maxlength="100" placeholder="ä¾å¦ï¼ççªçäº§åºæ¯æ¥ç¨æ°´é¢è¦" /></FormItem> |
| | | <div class="grid grid-cols-1 gap-x-4 sm:grid-cols-2"> |
| | | <FormItem label="è½æº"><Select v-model:value="model.energyType" :options="energyOptions" /></FormItem> |
| | | <FormItem label="ç»è®¡ææ "><Select v-model:value="model.metric" :options="metricOptions" /></FormItem> |
| | | <FormItem label="使ç¨åºåï¼ç²¾ç¡®å¹é
ï¼å¯ç空ï¼"><Input v-model:value="model.usageArea" :maxlength="100" placeholder="ç空ç»è®¡å
¨é¨åºå" /></FormItem> |
| | | <FormItem label="产åç±»å«"><Select v-model:value="model.productCategory" :options="productOptions" allow-clear placeholder="å
¨é¨äº§å" /></FormItem> |
| | | </div> |
| | | <FormItem :label="`${model.metric.startsWith('MONTH_') ? 'æ¯æ' : 'æ¯æ¥'}éå¼ï¼${unit}ï¼å¤§äºæ¤å¼æ¶éç¥ï¼`" name="threshold"><InputNumber v-model:value="model.threshold" :min="0.001" :precision="3" style="width: 100%" /></FormItem> |
| | | <FormItem label="éç¥æ¥æ¶äºº" name="recipientIds"><Select v-model:value="model.recipientIds" mode="multiple" :options="recipients" option-filter-prop="label" placeholder="éæ©æ¬ç«å
ç¨æ·" /></FormItem> |
| | | <FormItem label="å¯ç¨è§å"><Switch v-model:checked="model.enabled" /></FormItem> |
| | | </Form> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| | |
| | | import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'; |
| | | import { Page } from '@vben/common-ui'; |
| | | import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | import { Alert, Button, Card, DatePicker, Empty, Form, FormItem, Input, RadioGroup, Select, Space, Spin, Table, message } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | import { getEnergyAnalysis } from '#/api/energy'; |
| | | import { exportEnergyAnalysis, getEnergyAnalysis } from '#/api/energy'; |
| | | import { energyInfo, energyOptions, numberText, productOptions } from '../shared'; |
| | | |
| | | defineOptions({ name: 'EnergyAnalysis' }); |
| | |
| | | const data = ref<EnergyAnalysis>(); |
| | | const loading = ref(false); |
| | | const failed = ref(false); |
| | | const exporting = ref(false); |
| | | const selectedEnergy = ref<EnergyType>('WATER'); |
| | | const trendRef = ref<EchartsUIType>(); |
| | | const shareRef = ref<EchartsUIType>(); |
| | |
| | | const trendChart = useEcharts(trendRef); |
| | | const shareChart = useEcharts(shareRef); |
| | | const areaChart = useEcharts(areaRef); |
| | | const costRef = ref<EchartsUIType>(); |
| | | const areaShareRef = ref<EchartsUIType>(); |
| | | const usageRef = ref<EchartsUIType>(); |
| | | const costChart = useEcharts(costRef); |
| | | const areaShareChart = useEcharts(areaShareRef); |
| | | const usageChart = useEcharts(usageRef); |
| | | const toolbox = (name: string) => ({ right: 8, feature: { saveAsImage: { title: 'ä¸è½½ PNG å¾ç', name, pixelRatio: 2 } } }); |
| | | async function exportReport() { |
| | | exporting.value = true; |
| | | try { downloadFileFromBlobPart({ fileName: 'è½èåæ.xlsx', source: await exportEnergyAnalysis({ ...applied.value }) }); } |
| | | finally { exporting.value = false; } |
| | | } |
| | | const hasRecords = computed(() => (data.value?.recordCount ?? 0) > 0); |
| | | const summaryColumns = [ |
| | | { title: 'è½æº', dataIndex: 'energyType' }, |
| | |
| | | const values = new Map(data.value.trend.filter(row => row.energyType === selectedEnergy.value).map(row => [row.label, Number(row.quantity)])); |
| | | await trendChart.renderEcharts({ |
| | | animation: false, |
| | | toolbox: toolbox(`${info.name}ç¨éè¶å¿`), |
| | | tooltip: { trigger: 'axis', confine: true }, |
| | | grid: { left: 20, right: 24, top: 40, bottom: 70, containLabel: true }, |
| | | xAxis: { type: 'category', data: periods }, |
| | |
| | | dataZoom: [{ type: 'inside' }, { type: 'slider', height: 18, bottom: 12 }], |
| | | series: [{ name: `${info.name}ï¼${info.unit}ï¼`, type: 'line', showSymbol: periods.length < 40, connectNulls: false, itemStyle: { color: info.color }, data: periods.map(period => values.get(period) ?? null) }], |
| | | }); |
| | | const usage = data.value.areas.filter(row => row.energyType === selectedEnergy.value).sort((a, b) => Number(b.quantity) - Number(a.quantity)).slice(0, 10).reverse(); |
| | | await usageChart.renderEcharts({ |
| | | animation: false, toolbox: toolbox(`${info.name}åºåç¨é`), |
| | | tooltip: { trigger: 'axis', confine: true, axisPointer: { type: 'shadow' } }, |
| | | grid: { left: 16, right: 36, top: 40, bottom: 24, containLabel: true }, |
| | | xAxis: { type: 'value', name: info.unit }, |
| | | yAxis: { type: 'category', data: usage.map(row => row.label), axisLabel: { width: 100, overflow: 'truncate' } }, |
| | | series: [{ name: `${info.name}ï¼${info.unit}ï¼`, type: 'bar', itemStyle: { color: info.color }, data: usage.map(row => Number(row.quantity)) }], |
| | | }); |
| | | await costChart.renderEcharts({ |
| | | animation: false, toolbox: toolbox('è½æºè´¹ç¨è¶å¿'), |
| | | tooltip: { trigger: 'axis', confine: true }, legend: { bottom: 0 }, |
| | | grid: { left: 16, right: 24, top: 40, bottom: 70, containLabel: true }, |
| | | xAxis: { type: 'category', data: periods }, yAxis: { type: 'value', name: 'è´¹ç¨ï¼å
ï¼' }, |
| | | dataZoom: [{ type: 'inside' }], |
| | | series: energyOptions.map(option => { |
| | | const costs = new Map(data.value!.trend.filter(row => row.energyType === option.value).map(row => [row.label, Number(row.amount)])); |
| | | return { name: option.label, type: 'line' as const, showSymbol: periods.length < 40, connectNulls: false, |
| | | itemStyle: { color: energyInfo[option.value as EnergyType].color }, data: periods.map(period => costs.get(period) ?? null) }; |
| | | }), |
| | | }); |
| | | } |
| | | async function renderCharts() { |
| | | if (!hasRecords.value || !data.value) return; |
| | | await renderTrend(); |
| | | if (Number(data.value.totalAmount) > 0) await shareChart.renderEcharts({ |
| | | animation: false, |
| | | toolbox: toolbox('è½æºè´¹ç¨å æ¯'), |
| | | tooltip: { trigger: 'item', confine: true, formatter: '{b}ï¼{c} å
ï¼{d}%ï¼' }, |
| | | legend: { bottom: 0 }, |
| | | series: [{ type: 'pie', radius: '65%', center: ['50%', '45%'], label: { formatter: '{b}\n{d}%' }, data: data.value.summary.filter(row => Number(row.amount) > 0).map(row => ({ name: energyInfo[row.energyType].name, value: Number(row.amount), itemStyle: { color: energyInfo[row.energyType].color } })) }], |
| | | }); |
| | | const totals = new Map<string, number>(); |
| | | for (const row of data.value.areas) totals.set(row.label, (totals.get(row.label) ?? 0) + Number(row.amount)); |
| | | if (Number(data.value.totalAmount) > 0) { |
| | | const sorted = [...totals].filter(([, amount]) => amount > 0).sort((a, b) => b[1] - a[1]); |
| | | const shares = sorted.slice(0, 10).map(([name, value]) => ({ name, value })); |
| | | const others = sorted.slice(10).reduce((sum, [, amount]) => sum + amount, 0); |
| | | if (others > 0) shares.push({ name: 'å
¶ä»åºåå计', value: others }); |
| | | await areaShareChart.renderEcharts({ |
| | | animation: false, toolbox: toolbox('åºåè´¹ç¨å æ¯'), |
| | | tooltip: { trigger: 'item', confine: true, formatter: '{b}ï¼{c} å
ï¼{d}%ï¼' }, |
| | | legend: { type: 'scroll', bottom: 0 }, |
| | | series: [{ type: 'pie', radius: ['35%', '60%'], center: ['50%', '44%'], label: { formatter: '{b}\n{d}%', width: 85, overflow: 'truncate' }, data: shares }], |
| | | }); |
| | | } |
| | | const areaLabels = [...totals].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([label]) => label).reverse(); |
| | | await areaChart.renderEcharts({ |
| | | animation: false, |
| | | toolbox: toolbox('åºåè´¹ç¨åå¸'), |
| | | tooltip: { trigger: 'axis', confine: true, axisPointer: { type: 'shadow' } }, |
| | | legend: { bottom: 0 }, |
| | | grid: { left: 16, right: 24, top: 30, bottom: 45, containLabel: true }, |
| | |
| | | <FormItem label="è½æº"><Select v-model:value="filters.energyType" :options="energyOptions" allow-clear placeholder="å
¨é¨è½æº" style="width: 130px" /></FormItem> |
| | | <FormItem label="åºå"><Input v-model:value="filters.usageArea" placeholder="åºåå
³é®è¯" allow-clear style="width: 150px" /></FormItem> |
| | | <FormItem label="产å"><Select v-model:value="filters.productCategory" :options="productOptions" allow-clear placeholder="å
¨é¨ç±»å«" style="width: 150px" /></FormItem> |
| | | <FormItem><Space><Button type="primary" :loading="loading" @click="load">åæ</Button><Button @click="reset">éç½®</Button></Space></FormItem> |
| | | <FormItem><Space wrap><Button type="primary" :loading="loading" @click="load">åæ</Button><Button @click="reset">éç½®</Button><Button v-access:code="['energy:analysis:export']" :disabled="!data || loading" :loading="exporting" @click="exportReport">导åºåææ¥è¡¨</Button></Space></FormItem> |
| | | </Form> |
| | | </Card> |
| | | <Alert v-if="failed" type="error" show-icon message="è½èåæå 载失败ï¼è¯·ç¹å»åæéè¯ã" class="mb-4" /> |
| | |
| | | <div class="mb-4 grid grid-cols-1 gap-4 lg:grid-cols-2"> |
| | | <Card title="è½æºè´¹ç¨å æ¯"><EchartsUI v-if="Number(data.totalAmount) > 0" ref="shareRef" class="h-[330px] w-full" /><Empty v-else description="å°æªå½å
¥è´¹ç¨ï¼å¡«ååä»·åæ¾ç¤ºè´¹ç¨å æ¯ã" /></Card> |
| | | <Card title="åºåè´¹ç¨åå¸ï¼å 10ï¼"><EchartsUI ref="areaRef" class="h-[330px] w-full" /></Card> |
| | | <Card title="è½æºè´¹ç¨è¶å¿"><EchartsUI ref="costRef" class="h-[330px] w-full" /></Card> |
| | | <Card title="åºåè´¹ç¨å æ¯"><EchartsUI v-if="Number(data.totalAmount) > 0" ref="areaShareRef" class="h-[330px] w-full" /><Empty v-else description="å°æ è´¹ç¨æ°æ®" /></Card> |
| | | <Card class="lg:col-span-2" :title="`${energyInfo[selectedEnergy].name} · åºåç¨é对æ¯ï¼å 10ï¼`"><EchartsUI ref="usageRef" class="h-[330px] w-full" /></Card> |
| | | </div> |
| | | <p class="text-sm text-muted-foreground">å¾è¡¨å³ä¸è§å¯ä¸è½½ PNGï¼åææ¥è¡¨å
å«ç»è®¡å£å¾ãè½æºæ±æ»ãæ¥æè¶å¿ãåºåæ±æ»å个工ä½è¡¨ï¼ä½¿ç¨æè¿ä¸æ¬¡åæçç鿡件ã</p> |
| | | <Card title="è¶å¿æç»"> |
| | | <Table :columns="detailColumns" :data-source="data.trend" :row-key="row => `${row.label}-${row.energyType}`" :scroll="{ x: 600 }" :pagination="{ pageSize: 10, showSizeChanger: true }" size="small"> |
| | | <template #bodyCell="{ column, record }"> |
| | |
| | | import type { EnergyFilters, EnergyRecord } from '#/api/energy'; |
| | | import { computed, onMounted, reactive, ref } from 'vue'; |
| | | import { Page } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | import { Alert, AutoComplete, Button, Card, DatePicker, Form, FormItem, Input, InputNumber, Modal, Popconfirm, Select, Space, Table, Tag, message } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | import { createEnergyRecord, deleteEnergyRecord, getEnergyRecord, getEnergyRecordPage, updateEnergyRecord } from '#/api/energy'; |
| | | import { createEnergyRecord, deleteEnergyRecord, exportEnergyRecords, getEnergyRecord, getEnergyRecordPage, updateEnergyRecord } from '#/api/energy'; |
| | | import { areaOptions, energyInfo, energyOptions, numberText, productOptions, purposeOptions } from '../shared'; |
| | | |
| | | defineOptions({ name: 'EnergyRecord' }); |
| | | const filters = reactive<EnergyFilters>({}); |
| | | const rows = ref<EnergyRecord[]>([]); |
| | | const loading = ref(false); |
| | | const exporting = ref(false); |
| | | async function exportRecords() { |
| | | if (filters.startDate && filters.endDate && filters.startDate > filters.endDate) { message.warning('å¼å§æ¥æä¸è½æäºç»ææ¥æ'); return; } |
| | | exporting.value = true; |
| | | try { downloadFileFromBlobPart({ fileName: 'è½èå°è´¦.xlsx', source: await exportEnergyRecords({ ...filters }) }); } |
| | | finally { exporting.value = false; } |
| | | } |
| | | const loadError = ref(false); |
| | | const pagination = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true }); |
| | | const open = ref(false); |
| | |
| | | </Form> |
| | | <div class="mb-4 flex flex-wrap items-center justify-between gap-3"> |
| | | <span class="text-base font-semibold">è½èå°è´¦</span> |
| | | <Button v-access:code="['energy:record:create']" type="primary" @click="showForm()">æ°å¢è½èè®°å½</Button> |
| | | <Space><Button v-access:code="['energy:record:export']" :loading="exporting" @click="exportRecords">导åºå°è´¦</Button><Button v-access:code="['energy:record:create']" type="primary" @click="showForm()">æ°å¢è½èè®°å½</Button></Space> |
| | | </div> |
| | | <Alert v-if="loadError" type="error" message="è½èè®°å½å 载失败ï¼è¯·ç¹å»æ¥è¯¢éè¯ã" show-icon class="mb-4" /> |
| | | <Table :columns="columns" :data-source="rows" row-key="id" :loading="loading" :pagination="pagination" :scroll="{ x: 1420 }" :locale="{ emptyText: 'ææ è½èè®°å½ï¼è¯·æ°å¢æ¯æ¥ç¨è½ä¿¡æ¯' }" @change="tableChange"> |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import { submitFinancePayment } from '#/api/erp/finance/payment'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ErpFinancePaymentApi } from '#/api/erp/finance/payment'; |
| | | |
| | |
| | | /** æäº¤å®¡æ¹ - å
è·åæµç¨å表 */ |
| | | async function handleSubmit(row: ErpFinancePaymentApi.FinancePayment) { |
| | | const hideLoading = message.loading({ |
| | | content: 'è·åå®¡æ¹æµç¨...', |
| | | content: automaticApproval.value ? 'æ£å¨æäº¤çæ...' : 'è·åå®¡æ¹æµç¨...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | if (automaticApproval.value) { |
| | | await submitFinancePayment(row.id!, 'finance_payment_approve'); |
| | | hideLoading(); |
| | | message.success('å·²æäº¤çæ'); |
| | | handleRefresh(); |
| | | return; |
| | | } |
| | | const processList = await getFinancePaymentApproveProcessList(); |
| | | hideLoading(); |
| | | if (!processList || processList.length === 0) { |
| | |
| | | return; |
| | | } |
| | | // æå¼æµç¨éæ©å¼¹çª |
| | | |
| | | processModalApi.setData({ id: row.id, processList }).open(); |
| | | } catch { |
| | | hideLoading(); |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ErpPurchaseRequestApi } from '#/api/erp/purchase/request'; |
| | | |
| | |
| | | /** æäº¤å®¡æ¹ - å
è·åæµç¨å表 */ |
| | | async function handleSubmit(row: ErpPurchaseRequestApi.PurchaseRequest) { |
| | | const hideLoading = message.loading({ |
| | | content: 'è·åå®¡æ¹æµç¨...', |
| | | content: automaticApproval.value ? 'æ£å¨æäº¤çæ...' : 'è·åå®¡æ¹æµç¨...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | if (automaticApproval.value) { |
| | | await submitPurchaseRequest(row.id!, 'purchase_request_approve'); |
| | | hideLoading(); |
| | | message.success('å·²æäº¤çæ'); |
| | | handleRefresh(); |
| | | return; |
| | | } |
| | | const processList = await getApproveProcessList(row.id!); |
| | | hideLoading(); |
| | | if (!processList || processList.length === 0) { |
| | |
| | | return; |
| | | } |
| | | // æå¼æµç¨éæ©å¼¹çª |
| | | |
| | | processModalApi.setData({ id: row.id, processList }).open(); |
| | | } catch { |
| | | hideLoading(); |
| | |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'éæ©è®¢åæ¶é´', |
| | | showTime: true, |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | showTime: false, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | rules: 'required', |
| | |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | ...getRangePickerDefaultProps(), |
| | | showTime: false, |
| | | format: 'YYYY-MM-DD', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | |
| | | import { |
| | | erpCountInputFormatter, |
| | | erpPriceInputFormatter, |
| | | formatDateTime, |
| | | formatDate, |
| | | } from '@vben/utils'; |
| | | |
| | | import { Spin, Tag } from 'ant-design-vue'; |
| | |
| | | { |
| | | label: 'è®¢åæ¶é´', |
| | | field: 'orderTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { |
| | | label: 'å计æ°é', |
| | |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | render: (val) => formatDate(val) as string, |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | |
| | | import { downloadFileFromBlobPart, isEmpty } from '@vben/utils'; |
| | | |
| | | import { message, Tag, Tooltip } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | |
| | | } from '#/api/erp/sale/order'; |
| | | import { generateFromSaleOrder } from '#/api/mes/wm/salesnotice'; |
| | | import { $t } from '#/locales'; |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | |
| | | |
| | | /** 导åºè¡¨æ ¼ */ |
| | | async function handleExport() { |
| | | const data = await exportSaleOrder(await gridApi.formApi.getValues()); |
| | | const data = await exportSaleOrder( |
| | | normalizeDateRange(await gridApi.formApi.getValues()), |
| | | ); |
| | | downloadFileFromBlobPart({ fileName: 'éå®è®¢å.xls', source: data }); |
| | | } |
| | | |
| | | /** æ¥æçéè¦çæéèµ·æ¢æ¥æçå
¨å¤©ï¼å
¼å®¹å端æ¶é´ç±»åã */ |
| | | function normalizeDateRange(values: Record<string, any>) { |
| | | const range = values.orderTime; |
| | | if (!Array.isArray(range) || range.length !== 2 || !range.every(Boolean)) { |
| | | return values; |
| | | } |
| | | return { |
| | | ...values, |
| | | orderTime: [ |
| | | dayjs(range[0]).startOf('day').format('YYYY-MM-DD HH:mm:ss'), |
| | | dayjs(range[1]).endOf('day').format('YYYY-MM-DD HH:mm:ss'), |
| | | ], |
| | | }; |
| | | } |
| | | |
| | | /** 导å
¥éå®è®¢å */ |
| | |
| | | /** æäº¤éå®è®¢å审æ¹ï¼åååå
¬ï¼ */ |
| | | async function handleSubmit(row: ErpSaleOrderApi.SaleOrder) { |
| | | // è·åå¯ç¨çå®¡æ¹æµç¨å®ä¹ |
| | | const processList = await getSaleOrderApproveProcessList(); |
| | | if (isEmpty(processList)) { |
| | | const processList = automaticApproval.value ? [] : await getSaleOrderApproveProcessList(); |
| | | if (!automaticApproval.value && isEmpty(processList)) { |
| | | message.error('éå®è®¢åå®¡æ¹æµç¨æªé
ç½®ï¼è¯·å
å¨ãåååå
¬âæµç¨ç®¡çãä¸å建并åå¸éå®è®¢åå®¡æ¹æµç¨'); |
| | | return; |
| | | } |
| | | const hideLoading = message.loading({ |
| | | content: 'æ£å¨æäº¤å®¡æ¹...', |
| | | content: automaticApproval.value ? 'æ£å¨æäº¤çæ...' : 'æ£å¨æäº¤å®¡æ¹...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await submitSaleOrder(row.id!, processList[0].key); |
| | | message.success('æäº¤å®¡æ¹æåï¼è¯·å°åååå
¬ä¸å¤ç'); |
| | | await submitSaleOrder(row.id!, automaticApproval.value ? 'sale_order_approve' : processList[0]!.key); |
| | | message.success(automaticApproval.value ? 'å·²æäº¤çæ' : 'æäº¤å®¡æ¹æåï¼è¯·å°åååå
¬ä¸å¤ç'); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | // çäº§ç¶ææç´¢åæ°è½¬æ¢ï¼-1(æ éç产) â needProduction=0 |
| | | const { productionStatus, ...rest } = formValues; |
| | | const { productionStatus, ...rest } = normalizeDateRange(formValues); |
| | | const params: Record<string, any> = { ...rest }; |
| | | if (productionStatus === -1) { |
| | | params.needProduction = 0; |
| | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { formatDate } from '@vben/utils'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | |
| | | try { |
| | | formData.value = await getSaleOrder(data.id); |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(formData.value); |
| | | await formApi.setValues({ |
| | | ...formData.value, |
| | | productionFinishTime: formatDate(formData.value.productionFinishTime), |
| | | }); |
| | | if (formData.value?.attachmentList?.length) { |
| | | const blobIds = formData.value.attachmentList.map((item) => ({ |
| | | uid: String(item.id), |
| | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictLabel } from '@vben/hooks'; |
| | | import { useUserStore } from '@vben/stores'; |
| | | import { erpPriceMultiply, formatDateTime } from '@vben/utils'; |
| | | import { erpPriceMultiply, formatDate, formatDateTime } from '@vben/utils'; |
| | | |
| | | import { getCustomerSimpleList } from '#/api/crm/customer'; |
| | | import { getAccountSimpleList } from '#/api/erp/finance/account'; |
| | |
| | | <div>订ååå·ï¼{{ printData.no || '-' }}</div> |
| | | <div> |
| | | è®¢åæ¶é´ï¼{{ |
| | | formatDateTime(printData.orderTime) || '-' |
| | | formatDate(printData.orderTime) || '-' |
| | | }} |
| | | </div> |
| | | <div>客æ·ï¼{{ customerName }}</div> |
| | |
| | | <div>å建人ï¼{{ creatorName }}</div> |
| | | <div> |
| | | å建æ¶é´ï¼{{ |
| | | formatDateTime(printData.createTime) || '-' |
| | | formatDate(printData.createTime) || '-' |
| | | }} |
| | | </div> |
| | | <div class="col-span-3">夿³¨ï¼{{ printData.remark || '-' }}</div> |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmAttendanceExceptionApi } from '#/api/hrm/attendance/exception'; |
| | | |
| | |
| | | |
| | | /** æäº¤èå¤å¼å¸¸ç³è¯· */ |
| | | async function handleSubmit(row: HrmAttendanceExceptionApi.AttendanceException) { |
| | | const res = await getAttendanceExceptionApproveProcessList(); |
| | | const res = automaticApproval.value ? [] : await getAttendanceExceptionApproveProcessList(); |
| | | processList.value = res; |
| | | currentRow.value = row; |
| | | selectedProcessKey.value = undefined; |
| | | if (automaticApproval.value) { |
| | | selectedProcessKey.value = 'hrm_attendance_exception'; |
| | | await confirmSubmit(); |
| | | return; |
| | | } |
| | | submitModalVisible.value = true; |
| | | } |
| | | |
| | |
| | | submitLoading.value = true; |
| | | try { |
| | | await submitAttendanceException(currentRow.value!.id!, selectedProcessKey.value); |
| | | message.success('æäº¤å®¡æ¹æå'); |
| | | message.success(automaticApproval.value ? 'å·²æäº¤çæ' : 'æäº¤å®¡æ¹æå'); |
| | | submitModalVisible.value = false; |
| | | handleRefresh(); |
| | | } finally { |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmLeaveApi } from '#/api/hrm/leave'; |
| | | |
| | |
| | | |
| | | /** æäº¤è¯·åç³è¯·ï¼å
éæ©å®¡æ¹æµç¨ï¼ */ |
| | | async function handleSubmit(row: HrmLeaveApi.Leave) { |
| | | processList.value = await getLeaveApproveProcessDefinitionList(); |
| | | processList.value = automaticApproval.value ? [] : await getLeaveApproveProcessDefinitionList(); |
| | | currentRow.value = row; |
| | | selectedProcessKey.value = undefined; |
| | | if (automaticApproval.value) { |
| | | selectedProcessKey.value = 'hrm_leave'; |
| | | await confirmSubmit(); |
| | | return; |
| | | } |
| | | submitModalVisible.value = true; |
| | | } |
| | | |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmOvertimeApi } from '#/api/hrm/overtime'; |
| | | |
| | |
| | | const submitLoading = ref(false); |
| | | |
| | | async function handleSubmit(row: HrmOvertimeApi.Overtime) { |
| | | processList.value = await getOvertimeApproveProcessDefinitionList(); |
| | | processList.value = automaticApproval.value ? [] : await getOvertimeApproveProcessDefinitionList(); |
| | | currentRow.value = row; |
| | | selectedProcessKey.value = undefined; |
| | | if (automaticApproval.value) { |
| | | selectedProcessKey.value = 'hrm_overtime'; |
| | | await confirmSubmit(); |
| | | return; |
| | | } |
| | | submitModalVisible.value = true; |
| | | } |
| | | |
| | |
| | | submitLoading.value = true; |
| | | try { |
| | | await submitOvertime(currentRow.value!.id!, selectedProcessKey.value); |
| | | message.success('å·²æäº¤è³åååå
¬å®¡æ¹'); |
| | | message.success(automaticApproval.value ? 'å·²æäº¤çæ' : 'å·²æäº¤è³åååå
¬å®¡æ¹'); |
| | | submitModalVisible.value = false; |
| | | handleRefresh(); |
| | | } finally { |
| | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { DvMeteringSelect } from '#/views/mes/dv/metering/components'; |
| | | |
| | | /** 表åç±»å */ |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'checkUserId', |
| | | fieldName: 'checkUserName', |
| | | label: 'æ£æµäºº', |
| | | component: 'ApiSelect', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: getSimpleUserList, |
| | | labelField: 'nickname', |
| | | placeholder: 'è¯·éæ©æ£æµäºº', |
| | | valueField: 'id', |
| | | placeholder: '请è¾å
¥æ£æµäººå§å', |
| | | maxlength: 100, |
| | | }, |
| | | }, |
| | | { |
| | |
| | | }, |
| | | }, |
| | | { field: 'certNo', title: 'è¯ä¹¦ç¼å·', minWidth: 130 }, |
| | | { field: 'checkUserNickname', title: 'æ£æµäºº', minWidth: 100 }, |
| | | { field: 'checkUserName', title: 'æ£æµäºº', minWidth: 100 }, |
| | | { |
| | | field: 'nextCheckDate', |
| | | title: '䏿¬¡æ£æµæ¥æ', |
| | |
| | | |
| | | const displayLabel = computed(() => { |
| | | if (selectedItems.value.length === 0) return ''; |
| | | return selectedItems.value.map(item => item.name ?? '').join('ã'); |
| | | return selectedItems.value.map(item => |
| | | [item.name, item.specification, item.code ? `ï¼${item.code}ï¼` : ''].filter(Boolean).join(' '), |
| | | ).join('ã'); |
| | | }); |
| | | const showClear = computed( |
| | | () => |
| | |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥ç©æåç§°', |
| | | placeholder: 'æ¯æåç§°ä¸è§æ ¼ç»åæ¥è¯¢', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'specification', |
| | | label: 'è§æ ¼åå·', |
| | | component: 'Input', |
| | | componentProps: { allowClear: true, placeholder: '请è¾å
¥è§æ ¼åå·' }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥ç©æåç§°', |
| | | placeholder: 'æ¯æåç§°ä¸è§æ ¼ç»åæ¥è¯¢', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'specification', |
| | | label: 'è§æ ¼åå·', |
| | | component: 'Input', |
| | | componentProps: { allowClear: true, placeholder: '请è¾å
¥è§æ ¼åå·' }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import { submitPdProjectApproval } from '#/api/mes/pd/project'; |
| | | import type { FormType } from '../data'; |
| | | import type { MesPdProjectApi } from '#/api/mes/pd/project'; |
| | | |
| | |
| | | |
| | | /** æäº¤å®¡æ¹ - å
è·åæµç¨å表 */ |
| | | async function handleSubmit() { |
| | | const hideLoading = message.loading({ content: 'è·åå®¡æ¹æµç¨...', duration: 0 }); |
| | | const hideLoading = message.loading({ content: automaticApproval.value ? 'æ£å¨æäº¤çæ...' : 'è·åå®¡æ¹æµç¨...', duration: 0 }); |
| | | try { |
| | | if (automaticApproval.value) { |
| | | await submitPdProjectApproval(projectId.value, 'pd_project_approve'); |
| | | hideLoading(); |
| | | message.success('å·²æäº¤çæ'); |
| | | await loadProjectDetail(); |
| | | return; |
| | | } |
| | | const processList = await getPdProjectApproveProcessList(); |
| | | hideLoading(); |
| | | if (!processList || processList.length === 0) { |
| | |
| | | return; |
| | | } |
| | | // æå¼æµç¨éæ©å¼¹çª |
| | | |
| | | processModalApi.setData({ id: projectId.value, processList }).open(); |
| | | } catch { |
| | | hideLoading(); |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import { submitPdProjectApproval } from '#/api/mes/pd/project'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesPdProjectApi } from '#/api/mes/pd/project'; |
| | | |
| | |
| | | /** æäº¤å®¡æ¹ - å
è·åæµç¨å表 */ |
| | | async function handleSubmit(row: MesPdProjectApi.Project) { |
| | | const hideLoading = message.loading({ |
| | | content: 'è·åå®¡æ¹æµç¨...', |
| | | content: automaticApproval.value ? 'æ£å¨æäº¤çæ...' : 'è·åå®¡æ¹æµç¨...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | if (automaticApproval.value) { |
| | | await submitPdProjectApproval(row.id!, 'pd_project_approve'); |
| | | hideLoading(); |
| | | message.success('å·²æäº¤çæ'); |
| | | handleRefresh(); |
| | | return; |
| | | } |
| | | const processList = await getPdProjectApproveProcessList(); |
| | | hideLoading(); |
| | | if (!processList || processList.length === 0) { |
| | |
| | | return; |
| | | } |
| | | // æå¼æµç¨éæ©å¼¹çª |
| | | |
| | | processModalApi.setData({ id: row.id, processList }).open(); |
| | | } catch { |
| | | hideLoading(); |
| | |
| | | |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { getTemplateItemOptionList } from '#/api/mes/qc/template/item'; |
| | | import { getSupplierSimpleList } from '#/api/srm/supplier'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | | import { MdmItemSelect } from '#/views/basicData/mdm/components'; |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'vendorId', |
| | | fieldName: 'vendorName', |
| | | label: 'ä¾åºå', |
| | | component: 'ApiSelect', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: getSupplierSimpleList, |
| | | labelField: 'name', |
| | | placeholder: 'è¯·éæ©ä¾åºå', |
| | | valueField: 'id', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['sourceDocId'], |
| | | componentProps: (values) => ({ |
| | | disabled: !!values.sourceDocId, |
| | | allowClear: true, |
| | | api: getSupplierSimpleList, |
| | | labelField: 'name', |
| | | placeholder: 'è¯·éæ©ä¾åºå', |
| | | valueField: 'id', |
| | | }), |
| | | maxlength: 255, |
| | | placeholder: '请è¾å
¥ä¾åºååç§°ï¼å¯ç空ï¼', |
| | | }, |
| | | }, |
| | | { |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'vendorId', |
| | | fieldName: 'vendorName', |
| | | label: 'ä¾åºå', |
| | | component: 'ApiSelect', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: getSupplierSimpleList, |
| | | labelField: 'name', |
| | | placeholder: 'è¯·éæ©ä¾åºå', |
| | | valueField: 'id', |
| | | placeholder: '请è¾å
¥ä¾åºååç§°', |
| | | }, |
| | | }, |
| | | { |
| | |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | | field: 'vendorNickname', |
| | | title: 'ä¾åºåç®ç§°', |
| | | field: 'vendorName', |
| | | title: 'ä¾åºå', |
| | | width: 120, |
| | | }, |
| | | { |
| | |
| | | fileName: 'æ¥ææ£éªå导å
¥æ¨¡æ¿.xls', |
| | | hints: [ |
| | | 'ä»
å
许导å
¥ xlsãxlsx æ ¼å¼æä»¶ï¼å¿
å¡«åï¼æ£éªååç§°ã产åç©æç¼ç ãåæ ¼åæ°éãä¸åæ ¼åæ°éãæ£æµæ¥æãæ¥ææ¥æã', |
| | | 'ã产åç©æç¼ç ãéä¸ãç©æç®¡çãä¸çç¼ç å®å
¨ä¸è´ï¼ãä¾åºååç§°ãéä¸ãä¾åºå管çãä¸çåç§°ä¸è´ï¼ç空åè¯¥åæ ä¾åºåã', |
| | | 'ã产åç©æç¼ç ãéä¸ãç©æç®¡çãä¸çç¼ç å®å
¨ä¸è´ï¼ãä¾åºååç§°ãéå¿
å¡«ï¼å¯ç´æ¥å¡«ååç§°ï¼æ éå¨ç³»ç»ä¸å»ºç«ä¾åºåæ¡£æ¡ã', |
| | | 'åæ ¼åæ°é + ä¸åæ ¼åæ°é å¿
é¡»çäºãæ¬æ¬¡æ¥æ¶æ°éãï¼ç空æ¶èªå¨å两è
ä¹åï¼ï¼ãæ£æµç»æãæåå
¸åç§°å¡«åï¼æ ¡éªéè¿ / æ ¡éªä¸éè¿ï¼ã', |
| | | 'ãè´¨æ£æ¹æ¡ãå¡«æ¹æ¡åç§°ï¼ç空åæç©æèªå¨å¹é
IQC æ¹æ¡ï¼ãæ£æµäººåãç空åå½åç»å½ç¨æ·ï¼æ¥ææ¯æ 2026-01-01 10:00:00 æ 2026-01-01ã', |
| | | 'éè¡å¯¼å
¥ï¼å¤±è´¥è¡ä¸å½±åæåè¡ï¼å¯¼å
¥åçæ£éªå为ãå¾
æ£éªãç¶æï¼å¯å¨å表ä¸ç»§ç»ç¼è¾ãå½å
¥æ£æµç»æã', |
| | |
| | | label: '客æ·', |
| | | component: markRaw(CrmCustomerSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | allowClear: true, |
| | | placeholder: 'è¯·éæ©å®¢æ·ï¼å¯ä¸å¡«ï¼', |
| | | }, |
| | | rules: 'selectRequired', |
| | | dependencies: { |
| | | triggerFields: ['sourceDocId'], |
| | | componentProps: (values) => ({ |
| | | disabled: !!values.sourceDocId, |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | allowClear: true, |
| | | placeholder: 'è¯·éæ©å®¢æ·ï¼å¯ä¸å¡«ï¼', |
| | | }), |
| | | }, |
| | | }, |
| | |
| | | sourceDocType: row.sourceDocType, |
| | | sourceLineId: row.sourceLineId, |
| | | vendorId: row.vendorId, |
| | | vendorName: row.vendorName, |
| | | }; |
| | | iqcFormModalApi.setData({ formType: 'create', prefill }).open(); |
| | | } |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesProFeedbackApi } from '#/api/mes/pro/feedback'; |
| | | |
| | |
| | | </Popconfirm> |
| | | <!-- 审æ¹ä¸ï¼æ¥çå®¡æ¹ --> |
| | | <Button |
| | | v-if="row.status === STATUS_APPROVING" |
| | | v-if="!automaticApproval && row.status === STATUS_APPROVING" |
| | | type="link" |
| | | size="small" |
| | | @click="handleViewProcess(row)" |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import { submitApply } from '#/api/srm/supplierApply'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { SrmSupplierApplyApi } from '#/api/srm/supplierApply'; |
| | | |
| | |
| | | /** æäº¤å®¡æ¹ - å
è·ååååå
¬å®¡æ¹æµç¨å表 */ |
| | | async function handleSubmit(row: SrmSupplierApplyApi.ApplyVO) { |
| | | const hideLoading = message.loading({ |
| | | content: 'è·åå®¡æ¹æµç¨...', |
| | | content: automaticApproval.value ? 'æ£å¨æäº¤çæ...' : 'è·åå®¡æ¹æµç¨...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | if (automaticApproval.value) { |
| | | await submitApply(row.id!, 'supplier_apply_approve'); |
| | | hideLoading(); |
| | | message.success('å·²æäº¤çæ'); |
| | | handleRefresh(); |
| | | return; |
| | | } |
| | | const processList = await getApproveProcessList(); |
| | | hideLoading(); |
| | | if (!processList || processList.length === 0) { |
| | |
| | | return; |
| | | } |
| | | // æå¼æµç¨éæ©å¼¹çª |
| | | |
| | | processModalApi.setData({ id: row.id, processList }).open(); |
| | | } catch { |
| | | hideLoading(); |