feat: 完善课题、会员、财务、企微页面并新增资产/用车/采购
- 财务报销:发票 OCR 上传识别、发票类型、代发起
- 课题管理:联系电话校验、成员子表、成员统计导出与群发
- 会员管理:会费标识、我的资料、资料变更审核
- 资产管理:资产台账、领用与归还
- 用车管理:用车申请与审批流程选择
- 采购管理:采购单与商品明细
- 企业微信:通知多模板与推送时选择模板、活动 5 种模板与采集评价
Co-Authored-By: Claude <noreply@anthropic.com>
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace AssetItemApi { |
| | | /** èµäº§åç±» */ |
| | | export const Category = { |
| | | CONSUMABLE: 1, |
| | | OFFICE: 2, |
| | | FIXED: 3, |
| | | } as const; |
| | | |
| | | /** èµäº§å°è´¦ */ |
| | | export interface AssetItem { |
| | | id?: number; |
| | | assetNo?: string; |
| | | name: string; |
| | | category: number; |
| | | categoryName?: string; |
| | | spec?: string; |
| | | unit?: string; |
| | | quantity: number; |
| | | unitPrice?: number; |
| | | totalAmount?: number; |
| | | purchaseDate?: string; |
| | | custodianId?: number; |
| | | custodianName?: string; |
| | | location?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢èµäº§å页 */ |
| | | export function getAssetItemPage(params: PageParam) { |
| | | return requestClient.get<PageResult<AssetItemApi.AssetItem>>( |
| | | '/asset/item/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢èµäº§è¯¦æ
*/ |
| | | export function getAssetItem(id: number) { |
| | | return requestClient.get<AssetItemApi.AssetItem>(`/asset/item/get?id=${id}`); |
| | | } |
| | | |
| | | /** æ¥è¯¢èµäº§ç²¾ç®å表 */ |
| | | export function getAssetItemSimpleList() { |
| | | return requestClient.get<AssetItemApi.AssetItem[]>( |
| | | '/asset/item/simple-list', |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢èµäº§ */ |
| | | export function createAssetItem(data: AssetItemApi.AssetItem) { |
| | | return requestClient.post('/asset/item/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹èµäº§ */ |
| | | export function updateAssetItem(data: AssetItemApi.AssetItem) { |
| | | return requestClient.put('/asset/item/update', data); |
| | | } |
| | | |
| | | /** å é¤èµäº§ */ |
| | | export function deleteAssetItem(id: number) { |
| | | return requestClient.delete('/asset/item/delete', { params: { id } }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace AssetRequisitionApi { |
| | | /** å½è¿ç¶æ */ |
| | | export const ReturnStatus = { |
| | | NOT_RETURNED: 1, |
| | | PARTIAL: 2, |
| | | RETURNED: 3, |
| | | } as const; |
| | | |
| | | /** èµäº§é¢ç¨ */ |
| | | export interface AssetRequisition { |
| | | id?: number; |
| | | no?: string; |
| | | assetId?: number; |
| | | assetNo?: string; |
| | | assetName?: string; |
| | | category?: number; |
| | | categoryName?: string; |
| | | custodianId?: number; |
| | | custodianName?: string; |
| | | recipientId?: number; |
| | | recipientName?: string; |
| | | requisitionTime?: string; |
| | | quantity?: number; |
| | | purpose?: string; |
| | | expectedReturnDate?: string; |
| | | returnedQuantity?: number; |
| | | remainingQuantity?: number; |
| | | returnTime?: string; |
| | | returnStatus?: number; |
| | | returnStatusName?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | /** å½è¿è¯·æ± */ |
| | | export interface ReturnReq { |
| | | id: number; |
| | | returnQuantity: number; |
| | | returnTime?: string; |
| | | remark?: string; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢é¢ç¨å页 */ |
| | | export function getAssetRequisitionPage(params: PageParam) { |
| | | return requestClient.get<PageResult<AssetRequisitionApi.AssetRequisition>>( |
| | | '/asset/requisition/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢é¢ç¨è¯¦æ
*/ |
| | | export function getAssetRequisition(id: number) { |
| | | return requestClient.get<AssetRequisitionApi.AssetRequisition>( |
| | | `/asset/requisition/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢é¢ç¨ */ |
| | | export function createAssetRequisition( |
| | | data: AssetRequisitionApi.AssetRequisition, |
| | | ) { |
| | | return requestClient.post('/asset/requisition/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹é¢ç¨ï¼ä»
é¢ç¨äººãç¨éãé¢è®¡å½è¿æ¥æã夿³¨ï¼ */ |
| | | export function updateAssetRequisition( |
| | | data: AssetRequisitionApi.AssetRequisition, |
| | | ) { |
| | | return requestClient.put('/asset/requisition/update', data); |
| | | } |
| | | |
| | | /** å½è¿èµäº§ */ |
| | | export function returnAssetRequisition(data: AssetRequisitionApi.ReturnReq) { |
| | | return requestClient.put('/asset/requisition/return', data); |
| | | } |
| | | |
| | | /** å é¤é¢ç¨ */ |
| | | export function deleteAssetRequisition(id: number) { |
| | | return requestClient.delete('/asset/requisition/delete', { |
| | | params: { id }, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { FinanceReimbursementApi } from '../index'; |
| | | import type { SystemStorageApi } from '#/api/system/storage'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace FinanceReimbursementAiApi { |
| | | /** æ¥éå票 OCR è¯å«ç»æ */ |
| | | export interface OcrResultVO { |
| | | /** å票æä»¶ Blob ç¼å· */ |
| | | blobId?: number; |
| | | /** å票å·ç */ |
| | | invoiceNo?: string; |
| | | /** å票代ç ï¼æ°çµç¥¨å¯è½ä¸ºç©ºï¼ */ |
| | | invoiceCode?: string; |
| | | /** å票类å */ |
| | | invoiceType?: string; |
| | | /** è´ä¹°æ¹åç§°ï¼å票æ¬å¤´ï¼ */ |
| | | invoiceTitle?: string; |
| | | /** é宿¹åç§°ï¼å¼ç¥¨æ¹ï¼ */ |
| | | sellerName?: string; |
| | | /** ä»·ç¨å计éé¢ï¼åä½ï¼å
*/ |
| | | invoiceAmount?: number; |
| | | /** ç¨é¢å计ï¼åä½ï¼å
*/ |
| | | taxAmount?: number; |
| | | /** å¼ç¥¨æ¥æï¼yyyy-MM-dd */ |
| | | invoiceDate?: string; |
| | | /** 夿³¨ */ |
| | | remark?: string; |
| | | /** è¯å«å¤±è´¥æ¶çé误æç¤ºï¼æåæ¶ä¸ºç©ºï¼ */ |
| | | rawText?: string; |
| | | } |
| | | |
| | | /** OCR 确认ååå¡«ç»æ¥é表åçæ°æ® */ |
| | | export interface OcrImportPayload { |
| | | /** è¯å«æåçå票æç»è¡ */ |
| | | items: FinanceReimbursementApi.ReimbursementItem[]; |
| | | /** æ¬æ¬¡ä¸ä¼ çå票æä»¶ï¼éä¸å¹¶ä¿å为æ¥éåéä»¶ */ |
| | | blobs: SystemStorageApi.StorageBlob[]; |
| | | } |
| | | } |
| | | |
| | | /** æ¹éè¯å«æ¥éå票æä»¶ */ |
| | | export function ocrReimbursementInvoices(blobIds: number[]) { |
| | | return requestClient.post<FinanceReimbursementAiApi.OcrResultVO[]>( |
| | | '/finance/reimbursement/ai/ocr', |
| | | { blobIds }, |
| | | ); |
| | | } |
| | |
| | | id?: number; |
| | | reimbursementId?: number; |
| | | expenseType: number; // è´¹ç¨ç±»åï¼åå
¸ finance_expense_type |
| | | invoiceType?: string; // å票类åï¼å¢å¼ç¨ä¸ç¨å票/å¢å¼ç¨æ®éå票/çµåå票/æ¶æ®/å
¶ä» |
| | | invoiceNo?: string; // å票å·ç |
| | | invoiceAmount: number; // å票éé¢ |
| | | invoiceDate?: string; // åç¥¨æ¥æ yyyy-MM-dd |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace PurchaseOrderApi { |
| | | /** éè´ååæç» */ |
| | | export interface OrderItem { |
| | | id?: number; |
| | | orderId?: number; |
| | | itemName: string; |
| | | spec?: string; |
| | | quantity: number; |
| | | unitPrice: number; |
| | | amount?: number; |
| | | sortNo?: number; |
| | | } |
| | | |
| | | /** éè´å */ |
| | | export interface Order { |
| | | id?: number; |
| | | no?: string; |
| | | purchaseDate: string; |
| | | buyerId?: number; |
| | | buyerName: string; |
| | | totalAmount?: number; |
| | | status?: number; |
| | | remark?: string; |
| | | items?: OrderItem[]; |
| | | createTime?: string; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢éè´åå页 */ |
| | | export function getPurchaseOrderPage(params: PageParam) { |
| | | return requestClient.get<PageResult<PurchaseOrderApi.Order>>( |
| | | '/purchase/order/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢éè´å详æ
ï¼å«æç»ï¼ */ |
| | | export function getPurchaseOrder(id: number) { |
| | | return requestClient.get<PurchaseOrderApi.Order>( |
| | | `/purchase/order/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢éè´å */ |
| | | export function createPurchaseOrder(data: PurchaseOrderApi.Order) { |
| | | return requestClient.post('/purchase/order/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹éè´å */ |
| | | export function updatePurchaseOrder(data: PurchaseOrderApi.Order) { |
| | | return requestClient.put('/purchase/order/update', data); |
| | | } |
| | | |
| | | /** å é¤éè´å */ |
| | | export function deletePurchaseOrder(id: number) { |
| | | return requestClient.delete('/purchase/order/delete', { params: { id } }); |
| | | } |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace ResearchApplicationApi { |
| | | /** 课é¢ç³æ¥ä¹¦æåï¼åè¡¨ï¼ */ |
| | | export interface ApplicationMember { |
| | | id?: number; |
| | | applicationId?: number; |
| | | name: string; // æåå§å |
| | | contactPhone?: string; // èç³»çµè¯ |
| | | workUnit?: string; // æå±åä½ |
| | | jobTitle?: string; // èç§°/èå¡ |
| | | memberRole?: string; // æåè§è²ï¼ä¸»æäºº/æ ¸å¿æå/åä¸äºº |
| | | sortNo?: number; // æåºå· |
| | | } |
| | | |
| | | /** æå维度ç»è®¡ */ |
| | | export interface MemberStatistics { |
| | | name: string; // æåå§å |
| | | contactPhone?: string; // èç³»çµè¯ |
| | | workUnit?: string; // æå±åä½ |
| | | jobTitle?: string; // èç§°/èå¡ |
| | | memberRoles?: string; // æåè§è²ï¼å¤ä¸ªä»¥é¡¿å·åéï¼ |
| | | projectCount?: number; // åä¸è¯¾é¢æ° |
| | | projectTitles?: string; // åä¸è¯¾é¢åç§°ï¼åå·åéï¼ |
| | | } |
| | | |
| | | /** 课é¢ç³æ¥ä¹¦ */ |
| | | export interface Application { |
| | | id?: number; |
| | |
| | | jobTitle?: string; // èç§° |
| | | contactPhone?: string; // èç³»çµè¯ |
| | | contactEmail?: string; // èç³»é®ç®± |
| | | teamMembers?: string; // 课é¢ç»æåï¼æ¯è¡ä¸æ¡ï¼å§å|åä½|èç§°ï¼ |
| | | teamMembers?: string; // 课é¢ç»æåï¼æ§çææ¬ï¼ä»
å岿°æ®å±ç¤ºï¼ |
| | | members?: ApplicationMember[]; // 课é¢ç»æåå表 |
| | | budgetAmount?: number; // ç³è¯·ç»è´¹ï¼å
ï¼ |
| | | planContent?: string; // ç³æ¥ä¹¦æ£æ |
| | | status?: number; // ç¶æï¼åå
¸ research_application_status |
| | |
| | | }); |
| | | } |
| | | |
| | | /** ææå维度ç»è®¡è¯¾é¢å䏿
åµ */ |
| | | export function getMemberStatistics(params: { |
| | | keyword?: string; |
| | | status?: number; |
| | | }) { |
| | | return requestClient.get<ResearchApplicationApi.MemberStatistics[]>( |
| | | '/research/application/member-statistics', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** å¯¼åºæå维度ç»è®¡ Excel */ |
| | | export function exportMemberStatistics(params: { |
| | | keyword?: string; |
| | | status?: number; |
| | | }) { |
| | | return requestClient.download( |
| | | '/research/application/member-statistics/export-excel', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢æç³æ¥ä¹¦çä¸å®¶è¯å®¡æç»å表 */ |
| | | export function getTopicReviewList(applicationId: number) { |
| | | return requestClient.get<ResearchApplicationApi.TopicReview[]>( |
| | |
| | | industry?: string; // è¡ä¸ï¼åå
¸ ums_industry |
| | | joinDate?: string; // å
¥ä¼æ¥æ |
| | | expireDate?: string; // æææè³ |
| | | feePaid?: boolean; // æ¯å¦ç¼´çº³ä¼è´¹ |
| | | feeYear?: number; // 缴费年度ï¼å¦ 2026ï¼ |
| | | status?: number; // ç¶æï¼åå
¸ ums_member_status |
| | | archiveDocumentId?: number; // 彿¡£çæçæ¡£æ¡ IDï¼é空å³å·²å½æ¡£ï¼ |
| | | archived?: boolean; // æ¯å¦å·²å½æ¡£ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import type { UmsMemberApi } from './index'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace UmsMemberProfileApi { |
| | | /** èµæåæ´ç³è¯· */ |
| | | export interface ChangeRecord { |
| | | id?: number; |
| | | memberId?: number; |
| | | memberNo?: string; |
| | | memberName?: string; |
| | | userId?: number; |
| | | status?: number; // 0å¾
å®¡æ ¸ 10å·²éè¿ 20已驳å |
| | | contactUser?: string; |
| | | contactPhone?: string; |
| | | contactEmail?: string; |
| | | workUnit?: string; |
| | | title?: string; |
| | | researchDirection?: string; |
| | | unitIntro?: string; |
| | | legalPerson?: string; |
| | | organizationCode?: string; |
| | | regionCode?: string; |
| | | industry?: string; |
| | | changeSummary?: string; |
| | | auditUserId?: number; |
| | | auditTime?: string; |
| | | auditRemark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | /** æäº¤èµæåæ´ç³è¯· */ |
| | | export interface ApplyReq { |
| | | contactUser?: string; |
| | | contactPhone?: string; |
| | | contactEmail?: string; |
| | | workUnit?: string; |
| | | title?: string; |
| | | researchDirection?: string; |
| | | unitIntro?: string; |
| | | legalPerson?: string; |
| | | organizationCode?: string; |
| | | regionCode?: string; |
| | | industry?: string; |
| | | } |
| | | } |
| | | |
| | | /** è·å¾å½åç»å½ä¼åçä¸ªäººèµæ */ |
| | | export function getMyProfile() { |
| | | return requestClient.get<UmsMemberApi.Member>('/ums/member/profile/get'); |
| | | } |
| | | |
| | | /** è·å¾å½åä¼åæè¿ä¸æ¬¡èµæåæ´ç³è¯· */ |
| | | export function getMyLatestChange() { |
| | | return requestClient.get<UmsMemberProfileApi.ChangeRecord | null>( |
| | | '/ums/member/profile/change/latest', |
| | | ); |
| | | } |
| | | |
| | | /** æäº¤èµæåæ´ç³è¯· */ |
| | | export function applyProfileChange(data: UmsMemberProfileApi.ApplyReq) { |
| | | return requestClient.post<number>('/ums/member/profile/change/apply', data); |
| | | } |
| | | |
| | | /** 管çåå页æ¥è¯¢èµæåæ´ç³è¯· */ |
| | | export function getProfileChangePage( |
| | | params: PageParam & { |
| | | memberNo?: string; |
| | | memberName?: string; |
| | | status?: number; |
| | | }, |
| | | ) { |
| | | return requestClient.get<PageResult<UmsMemberProfileApi.ChangeRecord>>( |
| | | '/ums/member/profile/change/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** å®¡æ ¸éè¿ */ |
| | | export function approveProfileChange(id: number) { |
| | | return requestClient.put('/ums/member/profile/change/approve', null, { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** å®¡æ ¸é©³å */ |
| | | export function rejectProfileChange(id: number, remark: string) { |
| | | return requestClient.put('/ums/member/profile/change/reject', null, { |
| | | params: { id, remark }, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace VehicleApplicationApi { |
| | | /** ç¨è½¦ç³è¯·ç¶æ */ |
| | | export const Status = { |
| | | DRAFT: 0, |
| | | APPROVING: 15, |
| | | APPROVED: 20, |
| | | REJECTED: 35, |
| | | } as const; |
| | | |
| | | /** ç¨è½¦ç³è¯· */ |
| | | export interface Application { |
| | | id?: number; |
| | | no?: string; |
| | | applicantId?: number; |
| | | applicantName?: string; |
| | | useStartTime?: string; |
| | | useEndTime?: string; |
| | | departure?: string; |
| | | destination?: string; |
| | | passengers?: number; |
| | | reason?: string; |
| | | vehicleNo?: string; |
| | | status?: number; |
| | | statusName?: string; |
| | | processInstanceId?: string; |
| | | approveTime?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | /** å®¡æ¹æµç¨å®ä¹ */ |
| | | export interface ProcessDefinition { |
| | | id: string; |
| | | key: string; |
| | | name: string; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢ç¨è½¦ç³è¯·å页 */ |
| | | export function getVehicleApplicationPage(params: PageParam) { |
| | | return requestClient.get<PageResult<VehicleApplicationApi.Application>>( |
| | | '/vehicle/application/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢ç¨è½¦ç³è¯·è¯¦æ
*/ |
| | | export function getVehicleApplication(id: number) { |
| | | return requestClient.get<VehicleApplicationApi.Application>( |
| | | `/vehicle/application/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢ç¨è½¦ç³è¯· */ |
| | | export function createVehicleApplication( |
| | | data: VehicleApplicationApi.Application, |
| | | ) { |
| | | return requestClient.post('/vehicle/application/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹ç¨è½¦ç³è¯· */ |
| | | export function updateVehicleApplication( |
| | | data: VehicleApplicationApi.Application, |
| | | ) { |
| | | return requestClient.put('/vehicle/application/update', data); |
| | | } |
| | | |
| | | /** å é¤ç¨è½¦ç³è¯· */ |
| | | export function deleteVehicleApplication(id: number) { |
| | | return requestClient.delete('/vehicle/application/delete', { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** æäº¤ç¨è½¦å®¡æ¹ */ |
| | | export function submitVehicleApplication( |
| | | id: number, |
| | | processDefinitionKey: string, |
| | | ) { |
| | | return requestClient.post('/vehicle/application/submit', null, { |
| | | params: { id, processDefinitionKey }, |
| | | }); |
| | | } |
| | | |
| | | /** è·åç¨è½¦å®¡æ¹æµç¨å表 */ |
| | | export function getVehicleApproveProcessList() { |
| | | return requestClient.get<VehicleApplicationApi.ProcessDefinition[]>( |
| | | '/vehicle/application/approve-process-list', |
| | | ); |
| | | } |
| | |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | /** æ´»å¨æ¨¡æ¿ç±»å */ |
| | | export const ACTIVITY_TYPE = { |
| | | /** 纯éç¥ï¼åªæ¨ä¸æ¡ææ¬ï¼æ 表å */ |
| | | TEXT: 1, |
| | | /** åæ¡æ¶æ¯ï¼æ¨ä¸å¼ å¯ç¹å»å¡ç */ |
| | | MESSAGE: 2, |
| | | /** å¾çéç¥ï¼ä¸ä¼ å¾ç忍éå¾çæ¶æ¯ */ |
| | | IMAGE: 3, |
| | | /** éä»¶éç¥ï¼ä¸ä¼ æä»¶åæ¨éæä»¶æ¶æ¯ */ |
| | | FILE: 4, |
| | | /** æ°æ®éé/è¯ä»·ï¼æ¥å页 + èªå®ä¹å段 + è¯ä»· */ |
| | | FORM: 5, |
| | | } as const; |
| | | |
| | | export namespace WxCpActivityApi { |
| | | /** æ´»å¨ç¶æï¼0è稿 1å·²åå¸ 2å·²ç»æ 3已忶 */ |
| | | export const ACTIVITY_STATUS = { |
| | |
| | | USER: 2, |
| | | } as const; |
| | | |
| | | /** ééåæ®µ */ |
| | | export interface Field { |
| | | id?: number; |
| | | fieldLabel?: string; |
| | | /** text / textarea / number / date / select / radio / checkbox / rating */ |
| | | fieldType?: string; |
| | | /** å¤ä¸ªé项ç¨è±æéå·åé */ |
| | | fieldOptions?: string; |
| | | required?: boolean; |
| | | placeholder?: string; |
| | | sortNo?: number; |
| | | } |
| | | |
| | | /** æ´»å¨ */ |
| | | export interface Activity { |
| | | id?: number; |
| | | title?: string; |
| | | /** 模æ¿ç±»åï¼è§ ACTIVITY_TYPE */ |
| | | activityType?: number; |
| | | activityTypeName?: string; |
| | | content?: string; |
| | | /** å¡çæé®ææ¡ */ |
| | | buttonText?: string; |
| | | /** æ¯å¦å¼å¯è¯ä»·ï¼æ°æ®é鿍¡æ¿ï¼ */ |
| | | ratingEnabled?: boolean; |
| | | ratingTitle?: string; |
| | | ratingMax?: number; |
| | | /** å¾çéç¥çå¾ç blobId */ |
| | | imageBlobIds?: number[]; |
| | | imageAttachmentList?: SignupAttachment[]; |
| | | /** éä»¶éç¥çéä»¶ blobId */ |
| | | fileBlobIds?: number[]; |
| | | fileAttachmentList?: SignupAttachment[]; |
| | | /** æ°æ®é鿍¡æ¿çèªå®ä¹å段 */ |
| | | fieldList?: Field[]; |
| | | /** |
| | | * å°é¢å¾æä»¶ç¼å·ï¼æå¤ 1 å¼ ï¼ |
| | | * |
| | |
| | | blobIds?: number[]; |
| | | /** å°é¢å¾éä»¶å表ï¼å端è¿åï¼æå¤ 1 å¼ ï¼ */ |
| | | attachmentList?: SignupAttachment[]; |
| | | /** |
| | | * 详æ
å¾çæä»¶ç¼å·ï¼æå¤ 5 å¼ ï¼ |
| | | * |
| | | * ä¸å°é¢åæ ·èµ°éä»¶ä¸é´è¡¨ï¼ä½ recordType æ¯ wxcp_activity_detailã |
| | | * æ¥å页å¨ãæ´»å¨è¯¦æ
ãæå䏿¹æå徿¯ä¾å®æ´å±ç¤ºï¼ä¸è£åªï¼ï¼ |
| | | * äºç»´ç /æµ·æ¥è¿ç±»è¢«è£å°±å¤±æçå¾è¦æ¾è¿éï¼ä¸è¦æ¾å°é¢ã |
| | | */ |
| | | detailBlobIds?: number[]; |
| | | /** 详æ
å¾çéä»¶å表ï¼å端è¿åï¼æå¤ 5 å¼ ï¼ */ |
| | | detailAttachmentList?: SignupAttachment[]; |
| | | location?: string; |
| | | startTime?: string; |
| | | endTime?: string; |
| | |
| | | mobile?: string; |
| | | deptName?: string; |
| | | content?: string; |
| | | /** è¯ä»·æçº§ */ |
| | | ratingScore?: number; |
| | | /** èªå®ä¹ééåæ®µå¼ */ |
| | | fieldValues?: { fieldId?: number; fieldLabel?: string; fieldType?: string; fieldValue?: string }[]; |
| | | /** ç¶æï¼0å·²æ¥å 1已忶 2å·²ç¾å° */ |
| | | status?: number; |
| | | statusName?: string; |
| | |
| | | id?: number; |
| | | title?: string; |
| | | content?: string; |
| | | noticeType?: number; |
| | | noticeTypeName?: string; |
| | | summary?: string; |
| | | linkUrl?: string; |
| | | linkBtnText?: string; |
| | | coverUrl?: string; |
| | | fileBlobId?: number; |
| | | fileName?: string; |
| | | status?: number; |
| | | statusName?: string; |
| | | pushScope?: number; |
| | |
| | | id: number; |
| | | pushScope?: number; |
| | | pushTarget?: string; |
| | | noticeType?: number; |
| | | summary?: string; |
| | | linkUrl?: string; |
| | | linkBtnText?: string; |
| | | coverUrl?: string; |
| | | fileBlobId?: number; |
| | | fileName?: string; |
| | | }) { |
| | | return requestClient.post<{ |
| | | success: boolean; |
| | |
| | | return null; |
| | | }) |
| | | .filter(Boolean) as UploadProps['fileList']; |
| | | } else { |
| | | // v 为空ï¼null/undefinedï¼æ¶å¿
é¡»æ¸
空å表ï¼å¦ååæ¢è®°å½æ¶ï¼ä¾å¦ç¼è¾è¿å¸¦å°é¢çæ´»å¨ |
| | | // åç¹æ°å»ºãæè¯¦æ
æè½¬å°å¦ä¸æ¡ï¼ä¼æ®çä¸ä¸æ¡çå¾çï¼çèµ·æ¥æ¯ãå¾è¿å¨ä½æ¢ä¸äºã |
| | | fileList.value = []; |
| | | } |
| | | if (!isFirstRender.value) { |
| | | emit('change', value); |
| | |
| | | * @returns æ¯å¦å
许ä¸ä¼ |
| | | */ |
| | | async function beforeUpload(file: File) { |
| | | // æ£æ¥æä»¶æ°ééå¶ |
| | | if (fileList.value!.length >= props.maxNumber) { |
| | | // æ£æ¥æä»¶æ°ééå¶ã |
| | | // maxNumber=1 æ¶è¿éå¿
é¡»æ¾è¡ï¼å¦åãç¼è¾ãç±»åºæ¯ï¼è¿è¡¨åæ¶å·²æ 1 å¼ åæ¾å¾ï¼ |
| | | // 伿°¸è¿æ¢ä¸äºå¾ï¼antd ç onInternalChange å¨ maxCount===1 ä¸ä¼ slice(-1) åªçææ°ä¸å¼ ï¼ |
| | | // æ°å¾å¤©ç¶è¦çæ§å¾ï¼ä¸éè¦å¨ beforeUpload 鿦ã |
| | | if (fileList.value!.length >= props.maxNumber && props.maxNumber !== 1) { |
| | | message.error($t('ui.upload.maxNumber', [props.maxNumber])); |
| | | return Upload.LIST_IGNORE; |
| | | } |
| | |
| | | @preview="handlePreview" |
| | | @remove="handleRemove" |
| | | > |
| | | <!-- maxNumber=1 æ¶å³ä½¿å·²æä¸å¼ ä¹è¦ä¿çä¸ä¼ å
¥å£ï¼ç¨äºãæ¢ä¸å¼ ãï¼ï¼ |
| | | ææ§½ä¸æ¦ä¸ºç©ºï¼antd ä¼ç¨ appendActionVisible=false ææ´ä¸ªä¸ä¼ æé® v-show æï¼ |
| | | ç¼è¾åºæ¯ä¸å°±å½»åºæ²¡æå¯ç¹çä¸ä¼ å
¥å£äº --> |
| | | <div |
| | | v-if="fileList && fileList.length < maxNumber" |
| | | v-if="fileList && (fileList.length < maxNumber || maxNumber === 1)" |
| | | class="flex flex-col items-center justify-center" |
| | | > |
| | | <IconifyIcon icon="lucide:cloud-upload" /> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | /** èµäº§åç±»é项 */ |
| | | export const AssetCategoryOptions = [ |
| | | { label: 'æèå', value: 1 }, |
| | | { label: 'åå
¬èµäº§', value: 2 }, |
| | | { label: 'åºå®èµäº§', value: 3 }, |
| | | ]; |
| | | |
| | | /** èµäº§åç±»åç§° */ |
| | | export function getCategoryName(category?: number) { |
| | | return ( |
| | | AssetCategoryOptions.find((item) => item.value === category)?.label ?? '-' |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢/ä¿®æ¹è¡¨å */ |
| | | export function useFormSchema(formType: 'create' | 'edit' | 'detail') { |
| | | const disabled = formType === 'detail'; |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'assetNo', |
| | | label: 'èµäº§ç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'ç空åç±ç³»ç»èªå¨çæ', |
| | | maxlength: 32, |
| | | disabled, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'name', |
| | | label: 'èµäº§åç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'å¦ï¼æ¿å
æå°æº / A4 å¤å°çº¸', |
| | | maxlength: 128, |
| | | disabled, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'category', |
| | | label: 'èµäº§åç±»', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: AssetCategoryOptions, |
| | | placeholder: 'è¯·éæ©èµäº§åç±»', |
| | | allowClear: true, |
| | | disabled, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'spec', |
| | | label: 'è§æ ¼åå·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥è§æ ¼åå·', |
| | | maxlength: 128, |
| | | disabled, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'unit', |
| | | label: '计éåä½', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'å¦ï¼å° / 个 / ç®±', |
| | | maxlength: 16, |
| | | disabled, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'quantity', |
| | | label: 'åºåæ°é', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å½ååºåæ°é', |
| | | min: 0, |
| | | precision: 0, |
| | | class: '!w-full', |
| | | disabled, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'unitPrice', |
| | | label: 'åä»·ï¼å
ï¼', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥åä»·', |
| | | min: 0, |
| | | precision: 2, |
| | | class: '!w-full', |
| | | disabled, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'purchaseDate', |
| | | label: 'è´å
¥æ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©è´å
¥æ¥æ', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | style: { width: '100%' }, |
| | | disabled, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'custodianName', |
| | | label: 'ä¿ç®¡äººï¼ç®¡ç人ï¼', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥èµäº§ä¿ç®¡äººå§å', |
| | | maxlength: 64, |
| | | disabled, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'location', |
| | | label: 'åæ¾å°ç¹', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥åæ¾å°ç¹', |
| | | maxlength: 128, |
| | | disabled, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | autoSize: { minRows: 2, maxRows: 3 }, |
| | | disabled, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ] as VbenFormSchema[]; |
| | | } |
| | | |
| | | /** å表æç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'assetNo', |
| | | label: 'èµäº§ç¼å·', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥èµäº§ç¼å·', allowClear: true }, |
| | | }, |
| | | { |
| | | fieldName: 'name', |
| | | label: 'èµäº§åç§°', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥èµäº§åç§°', allowClear: true }, |
| | | }, |
| | | { |
| | | fieldName: 'category', |
| | | label: 'èµäº§åç±»', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: AssetCategoryOptions, |
| | | placeholder: 'è¯·éæ©èµäº§åç±»', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'custodianName', |
| | | label: 'ä¿ç®¡äºº', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥ä¿ç®¡äºº', allowClear: true }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表å */ |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { field: 'assetNo', title: 'èµäº§ç¼å·', width: 180, fixed: 'left' }, |
| | | { field: 'name', title: 'èµäº§åç§°', minWidth: 150 }, |
| | | { |
| | | field: 'category', |
| | | title: 'èµäº§åç±»', |
| | | width: 110, |
| | | slots: { default: 'category' }, |
| | | }, |
| | | { field: 'spec', title: 'è§æ ¼åå·', minWidth: 140 }, |
| | | { field: 'unit', title: 'åä½', width: 70 }, |
| | | { field: 'quantity', title: 'åºåæ°é', width: 100 }, |
| | | { field: 'unitPrice', title: 'åä»·ï¼å
ï¼', width: 110 }, |
| | | { field: 'totalAmount', title: 'æ»ä»·å¼ï¼å
ï¼', width: 120 }, |
| | | { field: 'custodianName', title: 'ä¿ç®¡äºº', width: 110 }, |
| | | { field: 'location', title: 'åæ¾å°ç¹', minWidth: 140 }, |
| | | { |
| | | title: 'æä½', |
| | | width: 180, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { AssetItemApi } from '#/api/asset/item'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { isEmpty } from '@vben/utils'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteAssetItem, |
| | | getAssetItemPage, |
| | | } from '#/api/asset/item'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | defineOptions({ name: 'AssetItem' }); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | function handleEdit(row: AssetItemApi.AssetItem) { |
| | | formModalApi.setData({ formType: 'edit', id: row.id }).open(); |
| | | } |
| | | |
| | | function handleDetail(row: AssetItemApi.AssetItem) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | async function handleDelete(ids: number[]) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting'), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await Promise.all(ids.map((id) => deleteAssetItem(id))); |
| | | message.success($t('ui.actionMessage.deleteSuccess')); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | const checkedIds = ref<number[]>([]); |
| | | function handleRowCheckboxChange({ records }: { records: AssetItemApi.AssetItem[] }) { |
| | | checkedIds.value = records.map((item) => item.id!); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getAssetItemPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { keyField: 'id', isHover: true }, |
| | | toolbarConfig: { refresh: true, search: true }, |
| | | } as VxeTableGridOptions<AssetItemApi.AssetItem>, |
| | | gridEvents: { |
| | | checkboxAll: handleRowCheckboxChange, |
| | | checkboxChange: handleRowCheckboxChange, |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <Grid table-title="èµäº§å°è´¦"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['èµäº§']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['asset:item:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: 'æ¹éå é¤', |
| | | type: 'primary', |
| | | danger: true, |
| | | disabled: isEmpty(checkedIds), |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['asset:item:delete'], |
| | | popConfirm: { |
| | | title: 'æ¯å¦å 餿éèµäº§ï¼ï¼å卿ªå½è¿é¢ç¨è®°å½æ¶ä¸å¯å é¤ï¼', |
| | | confirm: handleDelete.bind(null, checkedIds), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #category="{ row }"> |
| | | <Tag v-if="row.category === 1" color="default">æèå</Tag> |
| | | <Tag v-else-if="row.category === 2" color="processing">åå
¬èµäº§</Tag> |
| | | <Tag v-else color="gold">åºå®èµäº§</Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.detail'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['asset:item:query'], |
| | | onClick: handleDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['asset:item:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['asset:item:delete'], |
| | | popConfirm: { |
| | | title: `确认å é¤èµäº§ã${row.name}ãåï¼`, |
| | | confirm: handleDelete.bind(null, [row.id!]), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { AssetItemApi } from '#/api/asset/item'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { $t } from '@vben/locales'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createAssetItem, |
| | | getAssetItem, |
| | | updateAssetItem, |
| | | } from '#/api/asset/item'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | type FormType = 'create' | 'detail' | 'edit'; |
| | | |
| | | const formType = ref<FormType>('create'); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | labelWidth: 120, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'vertical', |
| | | schema: useFormSchema('create'), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = (await formApi.getValues()) as AssetItemApi.AssetItem; |
| | | try { |
| | | await (formType.value === 'create' |
| | | ? createAssetItem(data) |
| | | : updateAssetItem(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | formApi.setDisabled(data.formType === 'detail'); |
| | | formApi.updateSchema(useFormSchema(data.formType)); |
| | | if (!data.id) { |
| | | await formApi.setValues( |
| | | { category: 2, quantity: 1, unit: '个' }, |
| | | false, |
| | | ); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | const item = await getAssetItem(data.id); |
| | | await formApi.setValues(item, false); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | :title=" |
| | | formType === 'create' |
| | | ? $t('ui.actionTitle.create', ['èµäº§']) |
| | | : formType === 'edit' |
| | | ? $t('ui.actionTitle.edit', ['èµäº§']) |
| | | : 'èµäº§è¯¦æ
' |
| | | " |
| | | class="w-3/4" |
| | | :close-on-click-modal="false" |
| | | :show-confirm-button="formType !== 'detail'" |
| | | > |
| | | <Form class="mx-3" /> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import { getAssetItemSimpleList } from '#/api/asset/item'; |
| | | |
| | | import { AssetCategoryOptions } from '../item/data'; |
| | | |
| | | /** å½è¿ç¶æé项 */ |
| | | export const ReturnStatusOptions = [ |
| | | { label: 'æªå½è¿', value: 1 }, |
| | | { label: 'é¨åå½è¿', value: 2 }, |
| | | { label: 'å·²å½è¿', value: 3 }, |
| | | ]; |
| | | |
| | | export function getCategoryName(category?: number) { |
| | | return ( |
| | | AssetCategoryOptions.find((item) => item.value === category)?.label ?? '-' |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢/ä¿®æ¹è¡¨å */ |
| | | export function useFormSchema(formType: 'create' | 'edit' | 'detail') { |
| | | const disabled = formType === 'detail'; |
| | | const editDisabled = formType !== 'create'; |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'assetId', |
| | | label: 'é¢ç¨èµäº§', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: getAssetItemSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | placeholder: 'è¯·éæ©é¢ç¨èµäº§', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | disabled: disabled || editDisabled, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'recipientName', |
| | | label: 'é¢ç¨äºº', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥é¢ç¨äººå§å', |
| | | maxlength: 64, |
| | | disabled, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'requisitionTime', |
| | | label: 'é¢ç¨æ¶é´', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | showTime: true, |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | placeholder: 'è¯·éæ©é¢ç¨æ¶é´', |
| | | style: { width: '100%' }, |
| | | disabled: disabled || editDisabled, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'quantity', |
| | | label: 'é¢ç¨æ°é', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | min: 1, |
| | | precision: 0, |
| | | placeholder: '请è¾å
¥é¢ç¨æ°é', |
| | | class: '!w-full', |
| | | disabled: disabled || editDisabled, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'expectedReturnDate', |
| | | label: 'é¢è®¡å½è¿æ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | valueFormat: 'YYYY-MM-DD', |
| | | placeholder: 'è¯·éæ©é¢è®¡å½è¿æ¥æ', |
| | | style: { width: '100%' }, |
| | | disabled, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'purpose', |
| | | label: 'ç¨é', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥é¢ç¨ç¨é', |
| | | autoSize: { minRows: 2, maxRows: 3 }, |
| | | disabled, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | autoSize: { minRows: 2, maxRows: 3 }, |
| | | disabled, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ] as VbenFormSchema[]; |
| | | } |
| | | |
| | | /** å表æç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'no', |
| | | label: 'é¢ç¨åå·', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥é¢ç¨åå·', allowClear: true }, |
| | | }, |
| | | { |
| | | fieldName: 'assetName', |
| | | label: 'èµäº§åç§°', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥èµäº§åç§°', allowClear: true }, |
| | | }, |
| | | { |
| | | fieldName: 'recipientName', |
| | | label: 'é¢ç¨äºº', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥é¢ç¨äºº', allowClear: true }, |
| | | }, |
| | | { |
| | | fieldName: 'category', |
| | | label: 'èµäº§åç±»', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: AssetCategoryOptions, |
| | | placeholder: 'è¯·éæ©èµäº§åç±»', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'returnStatus', |
| | | label: 'å½è¿ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: ReturnStatusOptions, |
| | | placeholder: 'è¯·éæ©å½è¿ç¶æ', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表å */ |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { field: 'no', title: 'é¢ç¨åå·', width: 175, fixed: 'left' }, |
| | | { field: 'assetNo', title: 'èµäº§ç¼å·', width: 175 }, |
| | | { field: 'assetName', title: 'èµäº§åç§°', minWidth: 140 }, |
| | | { |
| | | field: 'category', |
| | | title: 'èµäº§åç±»', |
| | | width: 105, |
| | | slots: { default: 'category' }, |
| | | }, |
| | | { field: 'custodianName', title: 'ä¿ç®¡äºº', width: 100 }, |
| | | { field: 'recipientName', title: 'é¢ç¨äºº', width: 100 }, |
| | | { |
| | | field: 'requisitionTime', |
| | | title: 'é¢ç¨æ¶é´', |
| | | width: 165, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { field: 'quantity', title: 'é¢ç¨æ°é', width: 95 }, |
| | | { field: 'returnedQuantity', title: 'å·²å½è¿', width: 90 }, |
| | | { field: 'remainingQuantity', title: 'æªå½è¿', width: 90 }, |
| | | { |
| | | field: 'expectedReturnDate', |
| | | title: 'é¢è®¡å½è¿', |
| | | width: 115, |
| | | formatter: 'formatDate', |
| | | }, |
| | | { field: 'purpose', title: 'ç¨é', minWidth: 150, showOverflow: 'tooltip' }, |
| | | { |
| | | field: 'returnStatus', |
| | | title: 'å½è¿ç¶æ', |
| | | width: 100, |
| | | slots: { default: 'returnStatus' }, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 200, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { AssetRequisitionApi } from '#/api/asset/requisition'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteAssetRequisition, |
| | | getAssetRequisitionPage, |
| | | } from '#/api/asset/requisition'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import ReturnForm from './modules/return-form.vue'; |
| | | |
| | | defineOptions({ name: 'AssetRequisition' }); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ReturnModal, returnModalApi] = useVbenModal({ |
| | | connectedComponent: ReturnForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | function handleEdit(row: AssetRequisitionApi.AssetRequisition) { |
| | | formModalApi.setData({ formType: 'edit', id: row.id }).open(); |
| | | } |
| | | |
| | | function handleDetail(row: AssetRequisitionApi.AssetRequisition) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | function handleReturn(row: AssetRequisitionApi.AssetRequisition) { |
| | | returnModalApi.setData(row).open(); |
| | | } |
| | | |
| | | async function handleDelete(row: AssetRequisitionApi.AssetRequisition) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting'), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await deleteAssetRequisition(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess')); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getAssetRequisitionPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { keyField: 'id', isHover: true }, |
| | | toolbarConfig: { refresh: true, search: true }, |
| | | } as VxeTableGridOptions<AssetRequisitionApi.AssetRequisition>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ReturnModal @success="handleRefresh" /> |
| | | <Grid table-title="èµäº§é¢ç¨ç®¡ç"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'æ°å¢é¢ç¨', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['asset:requisition:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #category="{ row }"> |
| | | <Tag v-if="row.category === 1" color="default">æèå</Tag> |
| | | <Tag v-else-if="row.category === 2" color="processing">åå
¬èµäº§</Tag> |
| | | <Tag v-else color="gold">åºå®èµäº§</Tag> |
| | | </template> |
| | | <template #returnStatus="{ row }"> |
| | | <Tag v-if="row.returnStatus === 1" color="warning">æªå½è¿</Tag> |
| | | <Tag v-else-if="row.returnStatus === 2" color="processing">é¨åå½è¿</Tag> |
| | | <Tag v-else color="success">å·²å½è¿</Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.detail'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['asset:requisition:query'], |
| | | onClick: handleDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'å½è¿', |
| | | type: 'link', |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['asset:requisition:return'], |
| | | ifShow: () => (row.remainingQuantity ?? 0) > 0, |
| | | onClick: handleReturn.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['asset:requisition:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['asset:requisition:delete'], |
| | | ifShow: () => (row.returnedQuantity ?? 0) === 0, |
| | | popConfirm: { |
| | | title: `确认å é¤é¢ç¨è®°å½ã${row.no}ãåï¼å é¤åæªå½è¿æ°éå°éååºå`, |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { AssetRequisitionApi } from '#/api/asset/requisition'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { $t } from '@vben/locales'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createAssetRequisition, |
| | | getAssetRequisition, |
| | | updateAssetRequisition, |
| | | } from '#/api/asset/requisition'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | type FormType = 'create' | 'detail' | 'edit'; |
| | | |
| | | const formType = ref<FormType>('create'); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | labelWidth: 120, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'vertical', |
| | | schema: useFormSchema('create'), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = |
| | | (await formApi.getValues()) as AssetRequisitionApi.AssetRequisition; |
| | | try { |
| | | await (formType.value === 'create' |
| | | ? createAssetRequisition(data) |
| | | : updateAssetRequisition(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | formApi.setDisabled(data.formType === 'detail'); |
| | | formApi.updateSchema(useFormSchema(data.formType)); |
| | | if (!data.id) { |
| | | await formApi.setValues( |
| | | { requisitionTime: dayjs().format('YYYY-MM-DD HH:mm:ss') }, |
| | | false, |
| | | ); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | const requisition = await getAssetRequisition(data.id); |
| | | await formApi.setValues(requisition, false); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | :title=" |
| | | formType === 'create' |
| | | ? $t('ui.actionTitle.create', ['èµäº§é¢ç¨']) |
| | | : formType === 'edit' |
| | | ? $t('ui.actionTitle.edit', ['èµäº§é¢ç¨']) |
| | | : 'èµäº§é¢ç¨è¯¦æ
' |
| | | " |
| | | class="w-3/4" |
| | | :close-on-click-modal="false" |
| | | :show-confirm-button="formType !== 'detail'" |
| | | > |
| | | <Form class="mx-3" /> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { AssetRequisitionApi } from '#/api/asset/requisition'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { returnAssetRequisition } from '#/api/asset/requisition'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const current = ref<AssetRequisitionApi.AssetRequisition>(); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | labelWidth: 110, |
| | | }, |
| | | wrapperClass: 'grid-cols-1', |
| | | layout: 'vertical', |
| | | schema: [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'returnQuantity', |
| | | label: 'æ¬æ¬¡å½è¿æ°é', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | min: 1, |
| | | precision: 0, |
| | | class: '!w-full', |
| | | placeholder: '请è¾å
¥æ¬æ¬¡å½è¿æ°é', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'returnTime', |
| | | label: 'å½è¿æ¶é´', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | showTime: true, |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | style: { width: '100%' }, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ³¨ï¼éå¡«ï¼', |
| | | autoSize: { minRows: 2, maxRows: 3 }, |
| | | }, |
| | | }, |
| | | ], |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const values = |
| | | (await formApi.getValues()) as AssetRequisitionApi.ReturnReq; |
| | | try { |
| | | await returnAssetRequisition(values); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success('å½è¿æå'); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | current.value = undefined; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<AssetRequisitionApi.AssetRequisition>(); |
| | | current.value = data; |
| | | await formApi.setValues( |
| | | { |
| | | id: data.id, |
| | | returnQuantity: data.remainingQuantity ?? 1, |
| | | returnTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), |
| | | }, |
| | | false, |
| | | ); |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="èµäº§å½è¿" class="w-1/2" :close-on-click-modal="false"> |
| | | <div v-if="current" class="mx-3 mb-3 rounded bg-gray-50 p-3 text-sm"> |
| | | <div>èµäº§ï¼{{ current.assetName }}ï¼{{ current.assetNo }}ï¼</div> |
| | | <div>é¢ç¨äººï¼{{ current.recipientName }}</div> |
| | | <div> |
| | | é¢ç¨ {{ current.quantity }}ï¼å·²å½è¿ {{ current.returnedQuantity }}ï¼æªå½è¿ |
| | | {{ current.remainingQuantity }} |
| | | </div> |
| | | </div> |
| | | <Form class="mx-3" /> |
| | | </Modal> |
| | | </template> |
| | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'edit'; |
| | | |
| | | /** å票类åé项ï¼åºå®æä¸¾ï¼å¯¹åºåå° finance_reimbursement_item.invoice_typeï¼ */ |
| | | export const INVOICE_TYPE_OPTIONS = [ |
| | | { label: 'å¢å¼ç¨ä¸ç¨å票', value: 'å¢å¼ç¨ä¸ç¨å票' }, |
| | | { label: 'å¢å¼ç¨æ®éå票', value: 'å¢å¼ç¨æ®éå票' }, |
| | | { label: 'çµåå票', value: 'çµåå票' }, |
| | | { label: 'æ¶æ®', value: 'æ¶æ®' }, |
| | | { label: 'å
¶ä»', value: 'å
¶ä»' }, |
| | | ]; |
| | | |
| | | /** æ¥éåç¶æï¼å¯¹åºåå
¸ finance_reimbursement_statusï¼ */ |
| | | export const ReimbursementStatus = { |
| | | DRAFT: 0, // è稿 |
| | |
| | | placeholder: 'ç空åç±ç³»ç»èªå¨çæ', |
| | | maxlength: 36, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | }, |
| | | { |
| | | // æ¬äººåèµ·ï¼ç³è¯·äººåªè¯»ï¼é»è®¤å½åç»å½ç¨æ· |
| | | fieldName: 'applicantName', |
| | | label: 'ç³è¯·äºº', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'é»è®¤ä¸ºå½åç»å½ç¨æ·', |
| | | disabled: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['proxyFlag'], |
| | | show: (values) => !values.proxyFlag, |
| | | }, |
| | | }, |
| | | { |
| | | // 代ä»äººåèµ·ï¼éæ©å®é
ç³è¯·äººï¼ä»£å½äººç±å端åå»ºäººåæ®µè®°å½ |
| | | fieldName: 'applicantId', |
| | | label: 'å®é
ç³è¯·äºº', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©å®é
ç³è¯·äºº', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | api: getSimpleUserList, |
| | | labelField: 'nickname', |
| | | valueField: 'id', |
| | | disabled: formType === 'detail', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['proxyFlag'], |
| | | show: (values) => !!values.proxyFlag, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'proxyFlag', |
| | | label: '代ä»äººåèµ·', |
| | | component: 'Switch', |
| | | componentProps: { |
| | | checkedChildren: '代åèµ·', |
| | | unCheckedChildren: 'æ¬äºº', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => formType !== 'detail', |
| | | }, |
| | | }, |
| | | { |
| | |
| | | slots: { default: 'expenseType' }, |
| | | }, |
| | | { |
| | | field: 'invoiceType', |
| | | title: 'å票类å', |
| | | minWidth: 150, |
| | | slots: { default: 'invoiceType' }, |
| | | }, |
| | | { |
| | | field: 'invoiceNo', |
| | | title: 'å票å·ç ', |
| | | minWidth: 160, |
| | |
| | | ): FinanceReimbursementApi.ReimbursementItem[] { |
| | | return (items ?? []).map((item) => ({ |
| | | expenseType: item.expenseType, |
| | | invoiceType: item.invoiceType, |
| | | invoiceNo: item.invoiceNo, |
| | | invoiceAmount: item.invoiceAmount, |
| | | invoiceDate: item.invoiceDate, |
| | |
| | | }, |
| | | }, |
| | | { |
| | | field: 'invoiceType', |
| | | title: 'å票类å', |
| | | minWidth: 130, |
| | | formatter: ({ cellValue }) => textRender(cellValue), |
| | | }, |
| | | { |
| | | field: 'invoiceNo', |
| | | title: 'å票å·ç ', |
| | | minWidth: 160, |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | import type { FinanceReimbursementApi } from '#/api/finance/reimbursement'; |
| | | import type { SystemStorageApi } from '#/api/system/storage'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { $t } from '@vben/locales'; |
| | | import { useUserStore } from '@vben/stores'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | |
| | | import ItemForm from './item-form.vue'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const userStore = useUserStore(); |
| | | |
| | | /** å½åç»å½ç¨æ· IDï¼ç¨äºæ¬äººåèµ·æ¶åå¡«ç³è¯·äººï¼ */ |
| | | const currentUserId = computed(() => { |
| | | const id = Number(userStore.userInfo?.userId); |
| | | return Number.isFinite(id) && id > 0 ? id : undefined; |
| | | }); |
| | | |
| | | const formData = ref<FinanceReimbursementApi.Reimbursement>({ |
| | | title: '', |
| | |
| | | formApi.setValues({ totalAmount }, false); |
| | | } |
| | | |
| | | /** |
| | | * OCR è¯å«æ¶ä¸ä¼ çå票æä»¶åæ¥å°ãå票éä»¶ãï¼ä¿è¯è¯å«è¿çå票éåä¿åã |
| | | * æ blobId å»éï¼é¿å
åä¸å¼ å票被éå¤è¯å«æ¶åºç°éå¤éä»¶ã |
| | | */ |
| | | async function handleOcrBlobs(blobs: SystemStorageApi.StorageBlob[]) { |
| | | if (blobs.length === 0) { |
| | | return; |
| | | } |
| | | const values = await formApi.getValues(); |
| | | const existing = (values.blobIds ?? []) as Array< |
| | | | number |
| | | | { id?: number; name?: string; status?: string; uid?: string; url?: string } |
| | | >; |
| | | const existingFiles = existing.map((item) => |
| | | typeof item === 'number' |
| | | ? { uid: String(item), status: 'done', id: item } |
| | | : item, |
| | | ); |
| | | const existingIds = new Set( |
| | | existingFiles |
| | | .map((item) => item.id) |
| | | .filter((id): id is number => typeof id === 'number'), |
| | | ); |
| | | const appended = blobs |
| | | .filter((blob) => !existingIds.has(blob.id)) |
| | | .map((blob) => ({ |
| | | uid: String(blob.id), |
| | | name: blob.name || blob.originalFilename || '', |
| | | url: blob.url || blob.previewURL || '', |
| | | status: 'done', |
| | | id: blob.id, |
| | | })); |
| | | if (appended.length === 0) { |
| | | return; |
| | | } |
| | | await formApi.setValues( |
| | | { blobIds: [...existingFiles, ...appended] }, |
| | | false, |
| | | ); |
| | | } |
| | | |
| | | /** åå»ºææ´æ°æ¥éå */ |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | |
| | | return; |
| | | } |
| | | |
| | | modalApi.lock(); |
| | | const data = |
| | | (await formApi.getValues()) as FinanceReimbursementApi.Reimbursement; |
| | | const data = (await formApi.getValues()) as FinanceReimbursementApi.Reimbursement & { |
| | | proxyFlag?: boolean; |
| | | }; |
| | | // 代ä»äººåèµ·å¿
须鿩å®é
ç³è¯·äººï¼æ¬äººåèµ·æ¶ä»¥å½åç»å½ç¨æ·ä¸ºå |
| | | if (data.proxyFlag) { |
| | | if (!data.applicantId) { |
| | | message.error('代ä»äººåèµ·æ¶ï¼è¯·éæ©å®é
ç³è¯·äºº'); |
| | | return; |
| | | } |
| | | } else { |
| | | data.applicantId = currentUserId.value; |
| | | } |
| | | // proxyFlag ä»
ç¨äºå端交äºï¼ä¸æäº¤ç»å端 |
| | | delete data.proxyFlag; |
| | | // å票é件为å
¨éæ¿æ¢è¯ä¹ï¼æ¸
空å
¨é¨å票æ¶å¿
é¡»æ¾å¼ä¼ 空æ°ç»ï¼ |
| | | // ä¼ null å端ä¼è§ä¸ºãä¸å¨éä»¶ãï¼å¯¼è´å 餿ä½ä¸çæ |
| | | data.blobIds = data.blobIds ?? []; |
| | | modalApi.lock(); |
| | | try { |
| | | await (formType.value === 'create' |
| | | ? createReimbursement(data) |
| | |
| | | formApi.updateSchema(useFormSchema(formType.value)); |
| | | if (!data || !data.id) { |
| | | initialItems.value = []; |
| | | // æ°å»ºæ¶é»è®¤ç³è¯·æ¥æä¸ºä»å¤© |
| | | // æ°å»ºæ¶é»è®¤æ¬äººåèµ·ï¼ç³è¯·äººå±ç¤ºå½åç»å½ç¨æ·ï¼ç³è¯·æ¥æä¸ºä»å¤© |
| | | await formApi.setValues( |
| | | { applyDate: dayjs().format('YYYY-MM-DD') }, |
| | | { |
| | | applicantId: currentUserId.value, |
| | | applicantName: userStore.userInfo?.nickname ?? '', |
| | | proxyFlag: false, |
| | | applyDate: dayjs().format('YYYY-MM-DD'), |
| | | }, |
| | | false, |
| | | ); |
| | | return; |
| | |
| | | status: 'done', |
| | | id: item.id, |
| | | })); |
| | | // å®é
ç³è¯·äººä¸å½åç»å½ç¨æ·ä¸ä¸è´æ¶ï¼è§ä¸ºä»£ä»äººåèµ·ï¼åæ¾ç³è¯·äººéæ©å¨ |
| | | const isProxy = |
| | | formType.value === 'edit' && |
| | | formData.value.applicantId != null && |
| | | formData.value.applicantId !== currentUserId.value; |
| | | await formApi.setValues( |
| | | { |
| | | ...formData.value, |
| | | proxyFlag: isProxy, |
| | | // 表åéç items åªç¨äºæäº¤ï¼å¿
é¡»å¥æè¡ idï¼å端ãå
å åå¢ãä¼ä¸è½¯å è¡ä¸»é®å²çªï¼ |
| | | items: toSubmitItems(formData.value.items), |
| | | blobIds, |
| | |
| | | :items="initialItems" |
| | | :disabled="formType === 'detail'" |
| | | class="w-full" |
| | | @ocr-blobs="handleOcrBlobs" |
| | | @update:items="handleUpdateItems" |
| | | @update:total-amount="handleUpdateTotalAmount" |
| | | /> |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { FinanceReimbursementAiApi } from '#/api/finance/reimbursement/ai'; |
| | | import type { FinanceReimbursementApi } from '#/api/finance/reimbursement'; |
| | | import type { SystemStorageApi } from '#/api/system/storage'; |
| | | |
| | | import type { DefaultOptionType } from 'ant-design-vue/es/select'; |
| | | |
| | | import { nextTick, watch } from 'vue'; |
| | | import { nextTick, ref, watch } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { erpPriceInputFormatter } from '@vben/utils'; |
| | | |
| | | import { Button, DatePicker, Input, InputNumber, Select } from 'ant-design-vue'; |
| | | import { |
| | | Button, |
| | | DatePicker, |
| | | Input, |
| | | InputNumber, |
| | | Select, |
| | | message, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { toSubmitItems, useItemColumns } from '../data'; |
| | | import { |
| | | INVOICE_TYPE_OPTIONS, |
| | | toSubmitItems, |
| | | useItemColumns, |
| | | } from '../data'; |
| | | import OcrUpload from './ocr-upload.vue'; |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | |
| | | }, |
| | | ); |
| | | |
| | | const emit = defineEmits(['update:items', 'update:totalAmount']); |
| | | const emit = defineEmits<{ |
| | | 'ocr-blobs': [blobs: SystemStorageApi.StorageBlob[]]; |
| | | 'update:items': [items: FinanceReimbursementApi.ReimbursementItem[]]; |
| | | 'update:totalAmount': [totalAmount: number]; |
| | | }>(); |
| | | |
| | | const ocrUploadRef = ref<InstanceType<typeof OcrUpload>>(); |
| | | |
| | | const expenseTypeOptions = getDictOptions( |
| | | DICT_TYPE.FINANCE_EXPENSE_TYPE, |
| | |
| | | recalc(); |
| | | } |
| | | |
| | | /** OCR è¯å«ç»æï¼è¿½å 为æç»è¡ï¼å¹¶æå票æä»¶åæ¥ç»ä¸»è¡¨éä»¶ */ |
| | | async function handleOcrSuccess( |
| | | payload: FinanceReimbursementAiApi.OcrImportPayload, |
| | | ) { |
| | | for (const item of payload.items) { |
| | | await gridApi.grid.insertAt(item, -1); |
| | | } |
| | | recalc(); |
| | | message.success( |
| | | `已填å
¥ ${payload.items.length} æ¡å票æç»ï¼è¯·è¡¥å
è´¹ç¨ç±»ååæäº¤`, |
| | | ); |
| | | if (payload.blobs.length > 0) { |
| | | emit('ocr-blobs', payload.blobs); |
| | | } |
| | | } |
| | | |
| | | /** å 餿ç»è¡ */ |
| | | async function handleDelete(row: FinanceReimbursementApi.ReimbursementItem) { |
| | | await gridApi.grid.remove(row); |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <OcrUpload ref="ocrUploadRef" @success="handleOcrSuccess" /> |
| | | <Grid class="w-full"> |
| | | <template #expenseType="{ row }"> |
| | | <Select |
| | |
| | | :disabled="disabled" |
| | | :options="expenseTypeOptions" |
| | | placeholder="è¯·éæ©è´¹ç¨ç±»å" |
| | | allow-clear |
| | | @change="() => recalc()" |
| | | /> |
| | | </template> |
| | | <template #invoiceType="{ row }"> |
| | | <Select |
| | | v-model:value="row.invoiceType" |
| | | :disabled="disabled" |
| | | :options="INVOICE_TYPE_OPTIONS" |
| | | placeholder="è¯·éæ©å票类å" |
| | | allow-clear |
| | | @change="() => recalc()" |
| | | /> |
| | |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | <div v-if="!disabled" class="-mt-4 flex justify-center"> |
| | | <div v-if="!disabled" class="-mt-4 flex justify-center gap-2"> |
| | | <Button @click="ocrUploadRef?.open()">OCR è¯å«å票</Button> |
| | | <Button type="primary" ghost @click="handleAdd"> æ·»å æ¥éæç» </Button> |
| | | </div> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { UploadFile, UploadProps } from 'ant-design-vue'; |
| | | import type { FinanceReimbursementAiApi } from '#/api/finance/reimbursement/ai'; |
| | | import type { SystemStorageApi } from '#/api/system/storage'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { |
| | | Alert, |
| | | Button, |
| | | Modal, |
| | | Table, |
| | | Tag, |
| | | Upload, |
| | | message, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { ocrReimbursementInvoices } from '#/api/finance/reimbursement/ai'; |
| | | import { uploadFile } from '#/api/system/storage'; |
| | | |
| | | import { INVOICE_TYPE_OPTIONS } from '../data'; |
| | | |
| | | defineOptions({ name: 'FinanceReimbursementOcrUpload' }); |
| | | |
| | | const emit = defineEmits<{ |
| | | success: [payload: FinanceReimbursementAiApi.OcrImportPayload]; |
| | | }>(); |
| | | |
| | | /** å·²ä¸ä¼ æä»¶ï¼uid -> blobï¼ç¨äºå 餿件å确认åå¡« */ |
| | | const blobMap = new Map<string, SystemStorageApi.StorageBlob>(); |
| | | /** blobId -> æä»¶åï¼ç¨äºå±ç¤ºè¯å«å¤±è´¥åå */ |
| | | const blobNameMap = new Map<number, string>(); |
| | | |
| | | const open = ref(false); |
| | | const step = ref(0); // 0=ä¸ä¼ , 1=è¯å«ä¸, 2=é¢è§ç»æ |
| | | const fileList = ref<UploadFile[]>([]); |
| | | const blobCount = ref(0); |
| | | const uploading = ref(0); |
| | | const results = ref<FinanceReimbursementAiApi.OcrResultVO[]>([]); |
| | | |
| | | const columns = [ |
| | | { title: 'å票å·ç ', dataIndex: 'invoiceNo', key: 'invoiceNo', width: 170 }, |
| | | { title: 'å票类å', dataIndex: 'invoiceType', key: 'invoiceType', width: 160 }, |
| | | { title: 'å¼ç¥¨æ¥æ', dataIndex: 'invoiceDate', key: 'invoiceDate', width: 110 }, |
| | | { title: 'éé¢(å
)', dataIndex: 'invoiceAmount', key: 'invoiceAmount', width: 110 }, |
| | | { title: 'é宿¹', dataIndex: 'sellerName', key: 'sellerName' }, |
| | | { title: 'ç¶æ', key: 'status', width: 80 }, |
| | | ]; |
| | | |
| | | /** è¯å«æåçå票 */ |
| | | const successResults = computed(() => |
| | | results.value.filter((item) => !item.rawText), |
| | | ); |
| | | |
| | | /** è¯å«å¤±è´¥çæä»¶ååå */ |
| | | const failedMessages = computed(() => |
| | | results.value |
| | | .filter((item) => item.rawText) |
| | | .map( |
| | | (item) => |
| | | `${blobNameMap.get(item.blobId ?? -1) ?? 'æä»¶'}ï¼${item.rawText}`, |
| | | ), |
| | | ); |
| | | |
| | | function handleOpen() { |
| | | handleReset(); |
| | | open.value = true; |
| | | } |
| | | |
| | | /** èªå®ä¹ä¸ä¼ ï¼åªè´è´£ææä»¶ä¼ å°åå¨ï¼è¯å«ç±ãå¼å§è¯å«ãç»ä¸è§¦å */ |
| | | const handleUpload: UploadProps['customRequest'] = async (options) => { |
| | | const { file, onError, onSuccess } = options; |
| | | uploading.value += 1; |
| | | try { |
| | | const result = await uploadFile([file as File]); |
| | | const blob = result?.[0]; |
| | | if (!blob?.id) { |
| | | throw new Error('æä»¶ä¸ä¼ 失败'); |
| | | } |
| | | const uid = String((file as File & { uid?: string }).uid ?? blob.id); |
| | | blobMap.set(uid, blob); |
| | | blobNameMap.set(blob.id, blob.originalFilename || blob.name || ''); |
| | | blobCount.value = blobMap.size; |
| | | onSuccess?.(blob); |
| | | } catch (error) { |
| | | onError?.(error as Error); |
| | | } finally { |
| | | uploading.value -= 1; |
| | | } |
| | | }; |
| | | |
| | | function handleRemove(file: UploadFile) { |
| | | blobMap.delete(String(file.uid)); |
| | | blobCount.value = blobMap.size; |
| | | if (step.value === 2) { |
| | | handleReset(); |
| | | } |
| | | } |
| | | |
| | | /** å¼å§æ¹éè¯å« */ |
| | | async function handleStart() { |
| | | const blobs = [...blobMap.values()]; |
| | | if (blobs.length === 0) { |
| | | message.warning('请å
ä¸ä¼ å票æä»¶'); |
| | | return; |
| | | } |
| | | step.value = 1; |
| | | try { |
| | | results.value = |
| | | (await ocrReimbursementInvoices(blobs.map((blob) => blob.id))) ?? []; |
| | | if (successResults.value.length === 0) { |
| | | step.value = 0; |
| | | message.error( |
| | | failedMessages.value[0] ?? 'æªè¯å«å°åç¥¨æ°æ®ï¼è¯·æå¨å½å
¥æéæ°ä¸ä¼ ', |
| | | ); |
| | | return; |
| | | } |
| | | step.value = 2; |
| | | } catch { |
| | | step.value = 0; |
| | | } |
| | | } |
| | | |
| | | /** æ OCR è¿åçå票类åå½ä¸åä¸ºä¸ææä¸¾å¼ */ |
| | | function normalizeInvoiceType(raw?: string) { |
| | | if (!raw) { |
| | | return undefined; |
| | | } |
| | | if (raw.includes('æ¶æ®')) { |
| | | return 'æ¶æ®'; |
| | | } |
| | | if (raw.includes('ä¸ç¨')) { |
| | | return 'å¢å¼ç¨ä¸ç¨å票'; |
| | | } |
| | | // ãå¢å¼ç¨çµåæ®éå票ããçµååç¥¨ï¼æ®éå票ï¼ãç»ä¸å½ä¸ºçµåå票 |
| | | if (raw.includes('çµå')) { |
| | | return 'çµåå票'; |
| | | } |
| | | if (raw.includes('æ®é')) { |
| | | return 'å¢å¼ç¨æ®éå票'; |
| | | } |
| | | return INVOICE_TYPE_OPTIONS[INVOICE_TYPE_OPTIONS.length - 1]?.value; |
| | | } |
| | | |
| | | /** 确认è¯å«ç»æï¼åå¡«æç»ä¸éä»¶ */ |
| | | function handleConfirm() { |
| | | const items = successResults.value.map((item) => ({ |
| | | invoiceType: normalizeInvoiceType(item.invoiceType), |
| | | invoiceNo: item.invoiceNo, |
| | | invoiceAmount: item.invoiceAmount, |
| | | invoiceDate: item.invoiceDate, |
| | | // é宿¹å³æ¥éæç»ä¸çå¼ç¥¨æ¹ |
| | | drawer: item.sellerName, |
| | | })) as unknown as FinanceReimbursementAiApi.OcrImportPayload['items']; |
| | | emit('success', { items, blobs: [...blobMap.values()] }); |
| | | handleClose(); |
| | | } |
| | | |
| | | function handleReset() { |
| | | step.value = 0; |
| | | fileList.value = []; |
| | | blobMap.clear(); |
| | | blobNameMap.clear(); |
| | | blobCount.value = 0; |
| | | results.value = []; |
| | | } |
| | | |
| | | function handleClose() { |
| | | open.value = false; |
| | | handleReset(); |
| | | } |
| | | |
| | | defineExpose({ open: handleOpen }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | v-model:open="open" |
| | | title="OCR è¯å«å票" |
| | | width="760px" |
| | | :footer="null" |
| | | @cancel="handleClose" |
| | | > |
| | | <!-- Step 0: ä¸ä¼ --> |
| | | <template v-if="step === 0"> |
| | | <Upload.Dragger |
| | | v-model:file-list="fileList" |
| | | multiple |
| | | :max-count="20" |
| | | :custom-request="handleUpload" |
| | | accept=".pdf,.png,.jpg,.jpeg,.gif,.bmp" |
| | | @remove="handleRemove" |
| | | > |
| | | <p class="text-4xl text-gray-400"> |
| | | <IconifyIcon icon="ant-design:inbox-outlined" /> |
| | | </p> |
| | | <p class="text-base text-gray-500"> |
| | | ç¹å»æææ½å票å¾ç / PDF å°æ¤åºåä¸ä¼ |
| | | </p> |
| | | <p class="text-sm text-gray-400"> |
| | | æ¯æ JPGãPNGãBMPãGIFãPDFï¼æå¤ 20 个æä»¶ï¼ææºç«¯å¯ç´æ¥æç
§ä¸ä¼ |
| | | </p> |
| | | </Upload.Dragger> |
| | | <Alert |
| | | class="mt-3" |
| | | type="info" |
| | | show-icon |
| | | message="è¯å«ç»æä»
ç¨äºåå¡«ï¼å票å·ç ãéé¢ãæ¥æãé宿¹è¯·å¨æç»ä¸æ ¸å¯¹ååæäº¤ã" |
| | | /> |
| | | <div class="mt-4 flex justify-end gap-2"> |
| | | <Button @click="handleClose">åæ¶</Button> |
| | | <Button |
| | | type="primary" |
| | | :disabled="uploading > 0 || blobCount === 0" |
| | | :loading="uploading > 0" |
| | | @click="handleStart" |
| | | > |
| | | {{ uploading > 0 ? 'æä»¶ä¸ä¼ ä¸...' : `å¼å§è¯å«ï¼${blobCount}ï¼` }} |
| | | </Button> |
| | | </div> |
| | | </template> |
| | | |
| | | <!-- Step 1: è¯å«ä¸ --> |
| | | <template v-else-if="step === 1"> |
| | | <div class="flex flex-col items-center gap-4 py-12"> |
| | | <a-spin size="large" /> |
| | | <span class="text-base text-gray-500"> |
| | | æ£å¨æ¹éè¯å« {{ blobCount }} 个å票æä»¶ï¼è¯·ç¨å... |
| | | </span> |
| | | </div> |
| | | </template> |
| | | |
| | | <!-- Step 2: é¢è§ç»æ --> |
| | | <template v-else-if="step === 2"> |
| | | <Alert |
| | | v-if="failedMessages.length > 0" |
| | | class="mb-3" |
| | | type="warning" |
| | | show-icon |
| | | :message="`${failedMessages.length} 个æä»¶æªè¯å«æåï¼å¤±è´¥æä»¶ä»ä¼ä½ä¸ºå票éä»¶ä¿å`" |
| | | :description="failedMessages.join('ï¼')" |
| | | /> |
| | | <Table |
| | | :columns="columns" |
| | | :data-source="results" |
| | | :pagination="false" |
| | | bordered |
| | | size="small" |
| | | :scroll="{ y: 320 }" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'status'"> |
| | | <Tag v-if="record.rawText" color="red">失败</Tag> |
| | | <Tag v-else color="green">æå</Tag> |
| | | </template> |
| | | <template v-else-if="column.key === 'invoiceAmount'"> |
| | | {{ record.invoiceAmount ?? '-' }} |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | |
| | | <Alert |
| | | class="mt-3" |
| | | type="info" |
| | | show-icon |
| | | message="确认åï¼è¯å«æåçå票å°è¿½å 为æ¥éæç»è¡ï¼è´¹ç¨ç±»åéæå¨éæ©ï¼éé¢çä¿¡æ¯å¯ç»§ç»ä¿®æ¹ã" |
| | | /> |
| | | |
| | | <div class="mt-4 flex justify-end gap-2"> |
| | | <Button @click="handleReset">éæ°ä¸ä¼ </Button> |
| | | <Button |
| | | type="primary" |
| | | :disabled="successResults.length === 0" |
| | | @click="handleConfirm" |
| | | > |
| | | 确认并填å
¥ï¼{{ successResults.length }}ï¼ |
| | | </Button> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import type { PurchaseOrderApi } from '#/api/purchase/order'; |
| | | |
| | | /** æ°å¢/ä¿®æ¹è¡¨å */ |
| | | export function useFormSchema(formType: 'create' | 'edit' | 'detail') { |
| | | const disabled = formType === 'detail'; |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'purchaseDate', |
| | | label: 'éè´æ¶é´', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | valueFormat: 'YYYY-MM-DD', |
| | | placeholder: 'è¯·éæ©éè´æ¶é´', |
| | | style: { width: '100%' }, |
| | | disabled, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'buyerName', |
| | | label: 'éè´äººï¼ç»å人ï¼', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥éè´äººï¼ç»å人ï¼å§å', |
| | | maxlength: 64, |
| | | disabled, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | autoSize: { minRows: 2, maxRows: 3 }, |
| | | disabled, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'items', |
| | | label: 'éè´ååæç»', |
| | | component: 'Input', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ] as VbenFormSchema[]; |
| | | } |
| | | |
| | | /** å表æç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'no', |
| | | label: 'éè´åå·', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥éè´åå·', allowClear: true }, |
| | | }, |
| | | { |
| | | fieldName: 'buyerName', |
| | | label: 'éè´äºº', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥éè´äºº', allowClear: true }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表å */ |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { field: 'no', title: 'éè´åå·', width: 180, fixed: 'left' }, |
| | | { |
| | | field: 'purchaseDate', |
| | | title: 'éè´æ¶é´', |
| | | width: 120, |
| | | formatter: 'formatDate', |
| | | }, |
| | | { field: 'buyerName', title: 'éè´äººï¼ç»å人ï¼', minWidth: 140 }, |
| | | { field: 'totalAmount', title: 'éè´éé¢ï¼å
ï¼', width: 140 }, |
| | | { field: 'remark', title: '夿³¨', minWidth: 180, showOverflow: 'tooltip' }, |
| | | { |
| | | title: 'æä½', |
| | | width: 190, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** æç»è¡æäº¤å½¢æï¼å»æ id ä¸ orderIdï¼é¿å
软å 主é®å²çª */ |
| | | export function toSubmitItems( |
| | | items?: PurchaseOrderApi.OrderItem[], |
| | | ): PurchaseOrderApi.OrderItem[] { |
| | | return (items ?? []).map((item) => ({ |
| | | itemName: item.itemName, |
| | | spec: item.spec, |
| | | quantity: item.quantity, |
| | | unitPrice: item.unitPrice, |
| | | amount: item.amount, |
| | | sortNo: item.sortNo, |
| | | })); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { PurchaseOrderApi } from '#/api/purchase/order'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { isEmpty } from '@vben/utils'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deletePurchaseOrder, |
| | | getPurchaseOrderPage, |
| | | } from '#/api/purchase/order'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | defineOptions({ name: 'PurchaseOrder' }); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | function handleEdit(row: PurchaseOrderApi.Order) { |
| | | formModalApi.setData({ formType: 'edit', id: row.id }).open(); |
| | | } |
| | | |
| | | function handleDetail(row: PurchaseOrderApi.Order) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | async function handleDelete(ids: number[]) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting'), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await Promise.all(ids.map((id) => deletePurchaseOrder(id))); |
| | | message.success($t('ui.actionMessage.deleteSuccess')); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | const checkedIds = ref<number[]>([]); |
| | | function handleRowCheckboxChange({ |
| | | records, |
| | | }: { |
| | | records: PurchaseOrderApi.Order[]; |
| | | }) { |
| | | checkedIds.value = records.map((item) => item.id!); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getPurchaseOrderPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { keyField: 'id', isHover: true }, |
| | | toolbarConfig: { refresh: true, search: true }, |
| | | } as VxeTableGridOptions<PurchaseOrderApi.Order>, |
| | | gridEvents: { |
| | | checkboxAll: handleRowCheckboxChange, |
| | | checkboxChange: handleRowCheckboxChange, |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <Grid table-title="éè´å"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'æ°å¢éè´å', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['purchase:order:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: 'æ¹éå é¤', |
| | | type: 'primary', |
| | | danger: true, |
| | | disabled: isEmpty(checkedIds), |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['purchase:order:delete'], |
| | | popConfirm: { |
| | | title: 'æ¯å¦å 餿ééè´åï¼', |
| | | confirm: handleDelete.bind(null, checkedIds), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.detail'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['purchase:order:query'], |
| | | onClick: handleDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['purchase:order:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['purchase:order:delete'], |
| | | popConfirm: { |
| | | title: `确认å é¤éè´åã${row.no}ãåï¼`, |
| | | confirm: handleDelete.bind(null, [row.id!]), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { PurchaseOrderApi } from '#/api/purchase/order'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { $t } from '@vben/locales'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createPurchaseOrder, |
| | | getPurchaseOrder, |
| | | updatePurchaseOrder, |
| | | } from '#/api/purchase/order'; |
| | | |
| | | import { toSubmitItems, useFormSchema } from '../data'; |
| | | import ItemTable from './item-table.vue'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | type FormType = 'create' | 'detail' | 'edit'; |
| | | |
| | | const formType = ref<FormType>('create'); |
| | | const itemTableRef = ref<InstanceType<typeof ItemTable>>(); |
| | | const initialItems = ref<PurchaseOrderApi.OrderItem[]>([]); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | labelWidth: 130, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'vertical', |
| | | schema: useFormSchema('create'), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | const itemTable = Array.isArray(itemTableRef.value) |
| | | ? itemTableRef.value[0] |
| | | : itemTableRef.value; |
| | | try { |
| | | itemTable?.validate(); |
| | | } catch (error: any) { |
| | | message.error(error.message || 'éè´ååæç»éªè¯å¤±è´¥'); |
| | | return; |
| | | } |
| | | const items = itemTable?.getItems() ?? []; |
| | | modalApi.lock(); |
| | | const data = (await formApi.getValues()) as PurchaseOrderApi.Order; |
| | | data.items = items; |
| | | try { |
| | | await (formType.value === 'create' |
| | | ? createPurchaseOrder(data) |
| | | : updatePurchaseOrder(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | initialItems.value = []; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | formApi.setDisabled(data.formType === 'detail'); |
| | | formApi.updateSchema(useFormSchema(data.formType)); |
| | | if (!data.id) { |
| | | initialItems.value = []; |
| | | await formApi.setValues( |
| | | { purchaseDate: dayjs().format('YYYY-MM-DD') }, |
| | | false, |
| | | ); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | const order = await getPurchaseOrder(data.id); |
| | | initialItems.value = order.items ?? []; |
| | | await formApi.setValues( |
| | | { |
| | | ...order, |
| | | items: toSubmitItems(order.items), |
| | | }, |
| | | false, |
| | | ); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | :title=" |
| | | formType === 'create' |
| | | ? $t('ui.actionTitle.create', ['éè´å']) |
| | | : formType === 'edit' |
| | | ? $t('ui.actionTitle.edit', ['éè´å']) |
| | | : 'éè´å详æ
' |
| | | " |
| | | class="w-4/5" |
| | | :close-on-click-modal="false" |
| | | :show-confirm-button="formType !== 'detail'" |
| | | > |
| | | <Form class="mx-3"> |
| | | <template #items> |
| | | <ItemTable |
| | | ref="itemTableRef" |
| | | :items="initialItems" |
| | | :disabled="formType === 'detail'" |
| | | class="w-full" |
| | | @update:items="(items) => formApi.setValues({ items }, false)" |
| | | /> |
| | | </template> |
| | | </Form> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { PurchaseOrderApi } from '#/api/purchase/order'; |
| | | |
| | | import { nextTick, watch } from 'vue'; |
| | | |
| | | import { Button, Input, InputNumber } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { toSubmitItems } from '../data'; |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | items?: PurchaseOrderApi.OrderItem[]; |
| | | disabled?: boolean; |
| | | }>(), |
| | | { |
| | | items: () => [], |
| | | disabled: false, |
| | | }, |
| | | ); |
| | | |
| | | const emit = defineEmits<{ |
| | | 'update:items': [items: PurchaseOrderApi.OrderItem[]]; |
| | | }>(); |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: [ |
| | | { type: 'seq', title: 'åºå·', width: 55, fixed: 'left' }, |
| | | { |
| | | field: 'itemName', |
| | | title: 'åååç§°', |
| | | minWidth: 160, |
| | | slots: { default: 'itemName' }, |
| | | }, |
| | | { |
| | | field: 'spec', |
| | | title: 'è§æ ¼åå·', |
| | | minWidth: 150, |
| | | slots: { default: 'spec' }, |
| | | }, |
| | | { |
| | | field: 'quantity', |
| | | title: 'æ°é', |
| | | width: 120, |
| | | slots: { default: 'quantity' }, |
| | | }, |
| | | { |
| | | field: 'unitPrice', |
| | | title: 'åä»·ï¼å
ï¼', |
| | | width: 130, |
| | | slots: { default: 'unitPrice' }, |
| | | }, |
| | | { |
| | | field: 'amount', |
| | | title: 'éé¢ï¼å
ï¼', |
| | | width: 130, |
| | | slots: { default: 'amount' }, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 80, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ], |
| | | data: [], |
| | | minHeight: 180, |
| | | autoResize: true, |
| | | border: true, |
| | | rowConfig: { keyField: 'id', isHover: true }, |
| | | pagerConfig: { enabled: false }, |
| | | toolbarConfig: { enabled: false }, |
| | | } as VxeTableGridOptions<PurchaseOrderApi.OrderItem>, |
| | | }); |
| | | |
| | | function calcAmount(row: PurchaseOrderApi.OrderItem) { |
| | | const quantity = Number(row.quantity ?? 0); |
| | | const unitPrice = Number(row.unitPrice ?? 0); |
| | | return Math.round(quantity * unitPrice * 100) / 100; |
| | | } |
| | | |
| | | /** 读åå½åå
¨é¨æç»è¡ï¼æäº¤å½¢æï¼ */ |
| | | function collectItems(): PurchaseOrderApi.OrderItem[] { |
| | | const rows = |
| | | (gridApi.grid.getData() as PurchaseOrderApi.OrderItem[]) ?? []; |
| | | const removed = |
| | | (gridApi.grid.getRemoveRecords() as PurchaseOrderApi.OrderItem[]) ?? []; |
| | | const inserted = |
| | | (gridApi.grid.getInsertRecords() as PurchaseOrderApi.OrderItem[]) ?? []; |
| | | const isRemoved = (row: PurchaseOrderApi.OrderItem) => |
| | | removed.some( |
| | | (item) => item === row || (row.id != null && item.id === row.id), |
| | | ); |
| | | return toSubmitItems([ |
| | | ...rows.filter((row) => !isRemoved(row)), |
| | | ...inserted, |
| | | ]).map((item) => ({ ...item, amount: calcAmount(item as PurchaseOrderApi.OrderItem) })); |
| | | } |
| | | |
| | | function recalc() { |
| | | emit('update:items', collectItems()); |
| | | } |
| | | |
| | | async function handleAdd() { |
| | | await gridApi.grid.insertAt( |
| | | { quantity: 1, unitPrice: 0, sortNo: collectItems().length + 1 } as unknown as PurchaseOrderApi.OrderItem, |
| | | -1, |
| | | ); |
| | | recalc(); |
| | | } |
| | | |
| | | async function handleDelete(row: PurchaseOrderApi.OrderItem) { |
| | | await gridApi.grid.remove(row); |
| | | recalc(); |
| | | } |
| | | |
| | | /** æ ¡éªæç»ï¼åååç§°å¿
å¡«ï¼æ°é>0ï¼åä»·â¥0 */ |
| | | function validate() { |
| | | const rows = collectItems(); |
| | | if (rows.length === 0) { |
| | | throw new Error('éè´åè³å°éè¦ 1 è¡éè´ååæç»'); |
| | | } |
| | | for (let i = 0; i < rows.length; i++) { |
| | | const row = rows[i]!; |
| | | if (!row.itemName || String(row.itemName).trim() === '') { |
| | | throw new Error(`第 ${i + 1} è¡éè´ååï¼åååç§°ä¸è½ä¸ºç©º`); |
| | | } |
| | | if (!row.quantity || Number(row.quantity) <= 0) { |
| | | throw new Error(`第 ${i + 1} è¡éè´ååï¼æ°éå¿
é¡»å¤§äº 0`); |
| | | } |
| | | if (row.unitPrice == null || Number(row.unitPrice) < 0) { |
| | | throw new Error(`第 ${i + 1} è¡éè´ååï¼åä»·ä¸è½å°äº 0`); |
| | | } |
| | | } |
| | | } |
| | | |
| | | defineExpose({ validate, getItems: collectItems }); |
| | | |
| | | watch( |
| | | () => props.items, |
| | | async (items) => { |
| | | if (!items) { |
| | | return; |
| | | } |
| | | await nextTick(); |
| | | await gridApi.grid.loadData([...items]); |
| | | }, |
| | | { immediate: true }, |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <Grid class="w-full"> |
| | | <template #itemName="{ row }"> |
| | | <Input |
| | | v-model:value="row.itemName" |
| | | :disabled="disabled" |
| | | placeholder="请è¾å
¥åååç§°" |
| | | @change="() => recalc()" |
| | | /> |
| | | </template> |
| | | <template #spec="{ row }"> |
| | | <Input |
| | | v-model:value="row.spec" |
| | | :disabled="disabled" |
| | | placeholder="请è¾å
¥è§æ ¼åå·" |
| | | @change="() => recalc()" |
| | | /> |
| | | </template> |
| | | <template #quantity="{ row }"> |
| | | <InputNumber |
| | | v-model:value="row.quantity" |
| | | :disabled="disabled" |
| | | :min="0.01" |
| | | :precision="2" |
| | | class="!w-full" |
| | | @change="() => recalc()" |
| | | /> |
| | | </template> |
| | | <template #unitPrice="{ row }"> |
| | | <InputNumber |
| | | v-model:value="row.unitPrice" |
| | | :disabled="disabled" |
| | | :min="0" |
| | | :precision="2" |
| | | class="!w-full" |
| | | @change="() => recalc()" |
| | | /> |
| | | </template> |
| | | <template #amount="{ row }"> |
| | | <span>{{ calcAmount(row).toFixed(2) }}</span> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.delete'), |
| | | danger: true, |
| | | type: 'link', |
| | | icon: ACTION_ICON.DELETE, |
| | | disabled, |
| | | onClick: handleDelete.bind(null, row), |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | <div v-if="!disabled" class="-mt-4 flex justify-center"> |
| | | <Button type="primary" ghost @click="handleAdd"> æ·»å ååæç» </Button> |
| | | </div> |
| | | </template> |
| | |
| | | |
| | | import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ResearchApplicationApi } from '#/api/research/application'; |
| | | import type { ResearchGuidelineApi } from '#/api/research/guideline'; |
| | | |
| | | import { z } from '#/adapter/form'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'edit'; |
| | | |
| | | /** 课é¢ç»æåè§è²é项 */ |
| | | export const MEMBER_ROLE_OPTIONS = [ |
| | | { label: '主æäºº', value: '主æäºº' }, |
| | | { label: 'æ ¸å¿æå', value: 'æ ¸å¿æå' }, |
| | | { label: 'åä¸äºº', value: 'åä¸äºº' }, |
| | | ]; |
| | | |
| | | /** |
| | | * æåè¡æå½±ä¸ºå¯æäº¤å¯¹è±¡ï¼åªä¿çå端æ¥åçåæ®µï¼ |
| | | * ä¸å¸¦ id / applicationIdï¼é¿å
ä¸ãå
å åå¢ãç软å è¡ä¸»é®å²çªã |
| | | */ |
| | | export function toSubmitMembers( |
| | | members?: ResearchApplicationApi.ApplicationMember[], |
| | | ): ResearchApplicationApi.ApplicationMember[] { |
| | | return (members ?? []).map((member) => ({ |
| | | name: member.name, |
| | | contactPhone: member.contactPhone, |
| | | workUnit: member.workUnit, |
| | | jobTitle: member.jobTitle, |
| | | memberRole: member.memberRole, |
| | | sortNo: member.sortNo, |
| | | })); |
| | | } |
| | | |
| | | /** 课é¢ç³æ¥ä¹¦ç¶æï¼å¯¹åºåå
¸ research_application_statusï¼ */ |
| | | export const ApplicationStatus = { |
| | |
| | | label: 'èç³»çµè¯', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥èç³»çµè¯', |
| | | maxlength: 20, |
| | | placeholder: '请è¾å
¥ 11 使æºå·', |
| | | maxlength: 11, |
| | | disabled: readonly, |
| | | }, |
| | | rules: z |
| | | .string({ message: 'èç³»çµè¯ä¸è½ä¸ºç©º' }) |
| | | .min(1, 'èç³»çµè¯ä¸è½ä¸ºç©º') |
| | | .regex(/^1[3-9]\d{9}$/, 'èç³»çµè¯æ ¼å¼ä¸æ£ç¡®ï¼è¯·è¾å
¥æ£ç¡®ç 11 使æºå·'), |
| | | }, |
| | | { |
| | | fieldName: 'contactEmail', |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'teamMembers', |
| | | // å表ç¼è¾ç± form.vue ç #members ææ§½æ¸²æ |
| | | fieldName: 'members', |
| | | label: '课é¢ç»æå', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: 'æ¯è¡ä¸æ¡ï¼æ ¼å¼ï¼å§å|åä½|èç§°\nä¾å¦ï¼æå|XX大å¦|坿æ', |
| | | autoSize: { minRows: 3, maxRows: 6 }, |
| | | disabled: readonly, |
| | | }, |
| | | help: 'æ¯è¡ä¸æ¡ï¼ç¨ç«çº¿åéï¼å§å|åä½|èç§°', |
| | | component: 'Input', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; |
| | | |
| | | import { Spin, Tag } from 'ant-design-vue'; |
| | | import { Spin, Table, Tag } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getApplication, getTopicReviewList } from '#/api/research/application'; |
| | |
| | | function textRender(val: unknown) { |
| | | return val === 0 || val ? String(val) : '-'; |
| | | } |
| | | |
| | | /** 课é¢ç»æååªè¯»å */ |
| | | const memberColumns = [ |
| | | { title: 'å§å', dataIndex: 'name', key: 'name', width: 110 }, |
| | | { title: 'èç³»çµè¯', dataIndex: 'contactPhone', key: 'contactPhone', width: 130 }, |
| | | { title: 'æå±åä½', dataIndex: 'workUnit', key: 'workUnit' }, |
| | | { title: 'èç§°/èå¡', dataIndex: 'jobTitle', key: 'jobTitle', width: 120 }, |
| | | { title: 'æåè§è²', dataIndex: 'memberRole', key: 'memberRole', width: 110 }, |
| | | { title: 'æåº', dataIndex: 'sortNo', key: 'sortNo', width: 70 }, |
| | | ]; |
| | | |
| | | /** éé¢å段空å¼å ä½ */ |
| | | function amountRender(val: string | number | undefined) { |
| | |
| | | <Descriptions :data="formData" /> |
| | | </div> |
| | | |
| | | <!-- 课é¢ç»æå --> |
| | | <!-- 课é¢ç»æåï¼åè¡¨ï¼ --> |
| | | <div |
| | | v-if="formData?.teamMembers" |
| | | v-if="formData?.members?.length" |
| | | class="rounded-lg border border-border bg-background p-4" |
| | | > |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">课é¢ç»æå</span> |
| | | </div> |
| | | <Table |
| | | :columns="memberColumns" |
| | | :data-source="formData.members" |
| | | :pagination="false" |
| | | bordered |
| | | size="small" |
| | | row-key="id" |
| | | /> |
| | | </div> |
| | | |
| | | <!-- å岿°æ®å
¼å®¹ï¼æ§çææ¬æ ¼å¼ç课é¢ç»æå --> |
| | | <div |
| | | v-else-if="formData?.teamMembers" |
| | | class="rounded-lg border border-border bg-background p-4" |
| | | > |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">课é¢ç»æåï¼å岿æ¬ï¼</span> |
| | | </div> |
| | | <pre class="whitespace-pre-wrap break-all font-sans">{{ |
| | | formData.teamMembers |
| | | }}</pre> |
| | |
| | | } from './data'; |
| | | import FundPanel from './modules/fund-panel.vue'; |
| | | import Form from './modules/form.vue'; |
| | | import MemberStatisticsModal from './modules/member-statistics-modal.vue'; |
| | | import ProcessSelectModal from './modules/process-select-modal.vue'; |
| | | import ReviewPanel from './modules/review-panel.vue'; |
| | | |
| | |
| | | connectedComponent: FundPanel, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const memberStatisticsRef = ref<InstanceType<typeof MemberStatisticsModal>>(); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | |
| | | /** ç»è´¹æ¨ä»é¢æ¿ */ |
| | | function handleFund(row: ResearchApplicationApi.Application) { |
| | | fundModalApi.setData({ id: row.id }).open(); |
| | | } |
| | | |
| | | /** æå维度ç»è®¡ */ |
| | | function handleMemberStatistics() { |
| | | memberStatisticsRef.value?.open(); |
| | | } |
| | | |
| | | /** åå¸ç«é¡¹éç¥ */ |
| | |
| | | <ProcessModal @success="handleRefresh" /> |
| | | <ReviewModal @success="handleRefresh" /> |
| | | <FundModal @success="handleRefresh" /> |
| | | <MemberStatisticsModal ref="memberStatisticsRef" /> |
| | | <Grid table-title="课é¢ç³æ¥ä¹¦å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: 'æåç»è®¡', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['research:application:query'], |
| | | onClick: handleMemberStatistics, |
| | | }, |
| | | { |
| | | label: 'æ¹éå é¤', |
| | | type: 'primary', |
| | | danger: true, |
| | |
| | | getPublishedGuidelineList, |
| | | } from '#/api/research/guideline'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | import { toSubmitMembers, useFormSchema } from '../data'; |
| | | import MemberTable from './member-table.vue'; |
| | | |
| | | defineOptions({ name: 'ResearchApplicationForm' }); |
| | | |
| | |
| | | const formType = ref<FormType>('create'); |
| | | /** ä¾ãæå±è¯¾é¢æåã䏿ä¸ç±»å«å¸¦åºä½¿ç¨ */ |
| | | const guidelines = ref<ResearchGuidelineApi.Guideline[]>([]); |
| | | /** æåè¡¨æ ¼åæ¾æ°æ®æºï¼åªå¨æå¼å¼¹çªæ¶èµå¼ï¼å¢å æ¹åªéè¿ emit ååæµå°è¡¨åå¼ */ |
| | | const initialMembers = ref<ResearchApplicationApi.ApplicationMember[]>([]); |
| | | const memberTableRef = ref<InstanceType<typeof MemberTable>>(); |
| | | |
| | | const getTitle = computed(() => { |
| | | if (formType.value === 'create') { |
| | |
| | | jobTitle: values.jobTitle, |
| | | contactPhone: values.contactPhone, |
| | | contactEmail: values.contactEmail, |
| | | teamMembers: values.teamMembers, |
| | | members: toSubmitMembers(values.members), |
| | | budgetAmount: values.budgetAmount, |
| | | planContent: values.planContent, |
| | | remark: values.remark, |
| | |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | try { |
| | | memberTableRef.value?.validate(); |
| | | } catch (error) { |
| | | message.error((error as Error).message || '课é¢ç»æåæ ¡éªå¤±è´¥'); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | |
| | | schema: useFormSchema(formType.value, formApi, guidelines.value), |
| | | }); |
| | | formApi.setDisabled(formType.value === 'detail'); |
| | | // æååè¡¨åæ¾ï¼ä¿çå id ä¾è¡¨æ ¼è¡ key 使ç¨ï¼æäº¤æ¶åå¥ç¦» |
| | | initialMembers.value = application.members ?? []; |
| | | // å·²æé件转æä¸ä¼ ç»ä»¶å¯è¯å«çæä»¶å¯¹è±¡ï¼æäº¤æ¶æ valueKey=id æå为 blobId |
| | | const blobIds = (application.attachmentList ?? []).map((item) => ({ |
| | | uid: String(item.id), |
| | |
| | | status: 'done', |
| | | id: item.id, |
| | | })); |
| | | await formApi.setValues({ ...application, blobIds }, false); |
| | | await formApi.setValues( |
| | | { |
| | | ...application, |
| | | members: toSubmitMembers(application.members), |
| | | blobIds, |
| | | }, |
| | | false, |
| | | ); |
| | | } else { |
| | | guidelines.value = await getPublishedGuidelineList(); |
| | | formApi.setState({ |
| | | schema: useFormSchema(formType.value, formApi, guidelines.value), |
| | | }); |
| | | formApi.setDisabled(false); |
| | | initialMembers.value = []; |
| | | await formApi.resetForm(); |
| | | } |
| | | } finally { |
| | |
| | | :close-on-click-modal="false" |
| | | :show-confirm-button="formType !== 'detail'" |
| | | > |
| | | <Form class="mx-3" /> |
| | | <Form class="mx-3"> |
| | | <template #members> |
| | | <MemberTable |
| | | ref="memberTableRef" |
| | | :members="initialMembers" |
| | | :disabled="formType === 'detail'" |
| | | class="w-full" |
| | | @update:members="(members) => formApi.setValues({ members }, false)" |
| | | /> |
| | | </template> |
| | | </Form> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { TableColumnsType } from 'ant-design-vue'; |
| | | import type { DefaultOptionType } from 'ant-design-vue/es/select'; |
| | | import type { ResearchApplicationApi } from '#/api/research/application'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { |
| | | Button, |
| | | Input, |
| | | Modal, |
| | | Select, |
| | | Table, |
| | | message, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | exportMemberStatistics, |
| | | getMemberStatistics, |
| | | } from '#/api/research/application'; |
| | | |
| | | defineOptions({ name: 'ResearchMemberStatisticsModal' }); |
| | | |
| | | const open = ref(false); |
| | | const loading = ref(false); |
| | | const keyword = ref(''); |
| | | const status = ref<number | undefined>(); |
| | | const rows = ref<ResearchApplicationApi.MemberStatistics[]>([]); |
| | | |
| | | const statusOptions = getDictOptions( |
| | | DICT_TYPE.RESEARCH_APPLICATION_STATUS, |
| | | 'number', |
| | | ) as DefaultOptionType[]; |
| | | |
| | | const columns: TableColumnsType<ResearchApplicationApi.MemberStatistics> = [ |
| | | { title: 'æåå§å', dataIndex: 'name', key: 'name', width: 110, fixed: 'left' }, |
| | | { title: 'èç³»çµè¯', dataIndex: 'contactPhone', key: 'contactPhone', width: 130 }, |
| | | { title: 'æå±åä½', dataIndex: 'workUnit', key: 'workUnit', width: 180 }, |
| | | { title: 'èç§°/èå¡', dataIndex: 'jobTitle', key: 'jobTitle', width: 120 }, |
| | | { title: 'æåè§è²', dataIndex: 'memberRoles', key: 'memberRoles', width: 140 }, |
| | | { title: 'åä¸è¯¾é¢æ°', dataIndex: 'projectCount', key: 'projectCount', width: 110 }, |
| | | { title: 'åä¸è¯¾é¢', dataIndex: 'projectTitles', key: 'projectTitles', ellipsis: true }, |
| | | ]; |
| | | |
| | | const totalMembers = computed(() => rows.value.length); |
| | | |
| | | const queryParams = computed(() => ({ |
| | | keyword: keyword.value.trim() || undefined, |
| | | status: status.value, |
| | | })); |
| | | |
| | | async function loadData() { |
| | | loading.value = true; |
| | | try { |
| | | rows.value = (await getMemberStatistics(queryParams.value)) ?? []; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | function handleOpen() { |
| | | open.value = true; |
| | | keyword.value = ''; |
| | | status.value = undefined; |
| | | loadData(); |
| | | } |
| | | |
| | | async function handleExport() { |
| | | const data = await exportMemberStatistics(queryParams.value); |
| | | downloadFileFromBlobPart({ fileName: 'è¯¾é¢æåç»è®¡.xls', source: data }); |
| | | } |
| | | |
| | | /** çæå¯ç´æ¥åå°å¾®ä¿¡ç¾¤/ä¼ä¸å¾®ä¿¡çååææ¬ */ |
| | | function buildPublishText() { |
| | | const lines = ['ãè¯¾é¢æåååã']; |
| | | rows.value.forEach((row, index) => { |
| | | const meta = [row.workUnit, row.jobTitle].filter(Boolean).join('/'); |
| | | const metaText = meta ? `ï¼${meta}ï¼` : ''; |
| | | const roles = row.memberRoles ? `ï¼»${row.memberRoles}ï¼½` : ''; |
| | | const projects = row.projectTitles ? `ï¼${row.projectTitles}` : ''; |
| | | lines.push( |
| | | `${index + 1}. ${row.name}${metaText}${roles} åä¸è¯¾é¢ ${row.projectCount ?? 0} 项${projects}`, |
| | | ); |
| | | }); |
| | | return lines.join('\n'); |
| | | } |
| | | |
| | | async function handleCopy() { |
| | | if (rows.value.length === 0) { |
| | | message.warning('ææ å¯åå¸çæåæ°æ®'); |
| | | return; |
| | | } |
| | | const text = buildPublishText(); |
| | | try { |
| | | await navigator.clipboard.writeText(text); |
| | | message.success('æåååå·²å¤å¶ï¼å¯ç´æ¥ç²è´´å°ç¾¤å
åå¸'); |
| | | } catch { |
| | | // é HTTPS ç¯å¢ä¸ clipboard ä¸å¯ç¨æ¶ï¼éå为æå¨éæ©å¤å¶ |
| | | Modal.info({ |
| | | title: '请æå¨å¤å¶ä»¥ä¸åå', |
| | | content: text, |
| | | width: 640, |
| | | }); |
| | | } |
| | | } |
| | | |
| | | defineExpose({ open: handleOpen }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | v-model:open="open" |
| | | title="è¯¾é¢æåç»è®¡ï¼ææå维度ï¼" |
| | | width="1080px" |
| | | :footer="null" |
| | | > |
| | | <div class="mb-3 flex flex-wrap items-center gap-2"> |
| | | <Input |
| | | v-model:value="keyword" |
| | | placeholder="å§å / çµè¯ / åä½" |
| | | allow-clear |
| | | style="width: 220px" |
| | | @press-enter="loadData" |
| | | /> |
| | | <Select |
| | | v-model:value="status" |
| | | :options="statusOptions" |
| | | placeholder="ç³æ¥ç¶æï¼å
¨é¨ï¼" |
| | | allow-clear |
| | | style="width: 180px" |
| | | /> |
| | | <Button type="primary" @click="loadData">æ¥è¯¢</Button> |
| | | <div class="flex-1" /> |
| | | <span class="text-sm text-gray-500">å
± {{ totalMembers }} åæå</span> |
| | | <Button @click="handleCopy">å¤å¶ç¾¤ååå</Button> |
| | | <Button @click="handleExport">å¯¼åº Excel</Button> |
| | | </div> |
| | | |
| | | <Table |
| | | :columns="columns" |
| | | :data-source="rows" |
| | | :loading="loading" |
| | | :pagination="{ pageSize: 10, showSizeChanger: true }" |
| | | :scroll="{ x: 1100, y: 420 }" |
| | | bordered |
| | | size="small" |
| | | /> |
| | | |
| | | <div class="mt-2 text-xs text-gray-400"> |
| | | ç»è®¡å£å¾ï¼åä¸ãå§å + çµè¯ + åä½ãè§ä¸ºå䏿åï¼åä¸è¯¾é¢æ°æä¸éå¤è¯¾é¢è®¡æ°ã |
| | | </div> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ResearchApplicationApi } from '#/api/research/application'; |
| | | |
| | | import { nextTick, watch } from 'vue'; |
| | | |
| | | import { Button, Input, InputNumber, Select } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { MEMBER_ROLE_OPTIONS, toSubmitMembers } from '../data'; |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | members?: ResearchApplicationApi.ApplicationMember[]; |
| | | disabled?: boolean; |
| | | }>(), |
| | | { |
| | | members: () => [], |
| | | disabled: false, |
| | | }, |
| | | ); |
| | | |
| | | const emit = defineEmits<{ |
| | | 'update:members': [members: ResearchApplicationApi.ApplicationMember[]]; |
| | | }>(); |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: [ |
| | | { type: 'seq', title: 'åºå·', width: 55, fixed: 'left' }, |
| | | { |
| | | field: 'name', |
| | | title: 'å§å', |
| | | minWidth: 110, |
| | | slots: { default: 'name' }, |
| | | }, |
| | | { |
| | | field: 'contactPhone', |
| | | title: 'èç³»çµè¯', |
| | | minWidth: 140, |
| | | slots: { default: 'contactPhone' }, |
| | | }, |
| | | { |
| | | field: 'workUnit', |
| | | title: 'æå±åä½', |
| | | minWidth: 180, |
| | | slots: { default: 'workUnit' }, |
| | | }, |
| | | { |
| | | field: 'jobTitle', |
| | | title: 'èç§°/èå¡', |
| | | minWidth: 120, |
| | | slots: { default: 'jobTitle' }, |
| | | }, |
| | | { |
| | | field: 'memberRole', |
| | | title: 'æåè§è²', |
| | | minWidth: 130, |
| | | slots: { default: 'memberRole' }, |
| | | }, |
| | | { |
| | | field: 'sortNo', |
| | | title: 'æåº', |
| | | width: 90, |
| | | slots: { default: 'sortNo' }, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 80, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ], |
| | | data: [], |
| | | minHeight: 200, |
| | | autoResize: true, |
| | | border: true, |
| | | rowConfig: { keyField: 'id', isHover: true }, |
| | | pagerConfig: { enabled: false }, |
| | | toolbarConfig: { enabled: false }, |
| | | } as VxeTableGridOptions<ResearchApplicationApi.ApplicationMember>, |
| | | }); |
| | | |
| | | /** 读åå½åå
¨é¨æåè¡ï¼æäº¤å½¢æï¼ä¸å« id / applicationIdï¼ */ |
| | | function collectMembers(): ResearchApplicationApi.ApplicationMember[] { |
| | | const rows = |
| | | (gridApi.grid.getData() as ResearchApplicationApi.ApplicationMember[]) ?? |
| | | []; |
| | | const removed = |
| | | (gridApi.grid.getRemoveRecords() as ResearchApplicationApi.ApplicationMember[]) ?? |
| | | []; |
| | | const inserted = |
| | | (gridApi.grid.getInsertRecords() as ResearchApplicationApi.ApplicationMember[]) ?? |
| | | []; |
| | | const isRemoved = (row: ResearchApplicationApi.ApplicationMember) => |
| | | removed.some( |
| | | (item) => item === row || (row.id != null && item.id === row.id), |
| | | ); |
| | | return toSubmitMembers([ |
| | | ...rows.filter((row) => !isRemoved(row)), |
| | | ...inserted, |
| | | ]); |
| | | } |
| | | |
| | | /** 忥æåæ°æ®ç»ç¶ç»ä»¶ */ |
| | | function recalc() { |
| | | emit('update:members', collectMembers()); |
| | | } |
| | | |
| | | /** æ·»å æåè¡ */ |
| | | async function handleAdd() { |
| | | await gridApi.grid.insertAt( |
| | | { |
| | | memberRole: 'åä¸äºº', |
| | | sortNo: collectMembers().length + 1, |
| | | } as unknown as ResearchApplicationApi.ApplicationMember, |
| | | -1, |
| | | ); |
| | | recalc(); |
| | | } |
| | | |
| | | /** å 餿åè¡ */ |
| | | async function handleDelete(row: ResearchApplicationApi.ApplicationMember) { |
| | | await gridApi.grid.remove(row); |
| | | recalc(); |
| | | } |
| | | |
| | | /** æ ¡éªæåï¼å§åå¿
å¡«ï¼å¡«åçµè¯æ¶é¡»ä¸º 11 使æºå· */ |
| | | function validate() { |
| | | const rows = collectMembers(); |
| | | for (let i = 0; i < rows.length; i++) { |
| | | const row = rows[i]; |
| | | if (!row?.name || String(row.name).trim() === '') { |
| | | throw new Error(`第 ${i + 1} è¡æåï¼è¯·å¡«åå§å`); |
| | | } |
| | | const phone = row.contactPhone ? String(row.contactPhone).trim() : ''; |
| | | if (phone !== '' && !/^1[3-9]\d{9}$/.test(phone)) { |
| | | throw new Error( |
| | | `第 ${i + 1} è¡æå ${row.name}ï¼èç³»çµè¯æ ¼å¼ä¸æ£ç¡®ï¼è¯·è¾å
¥ 11 使æºå·`, |
| | | ); |
| | | } |
| | | } |
| | | } |
| | | |
| | | defineExpose({ validate }); |
| | | |
| | | /** |
| | | * åæ¾æåï¼ååï¼ç¶ â è¡¨æ ¼ï¼ã |
| | | * watcher å
ç¦æ¢åä¼ emitï¼é¿å
prop â emit æ»å¾ªç¯ã |
| | | */ |
| | | watch( |
| | | () => props.members, |
| | | async (members) => { |
| | | if (!members) { |
| | | return; |
| | | } |
| | | await nextTick(); |
| | | await gridApi.grid.loadData([...members]); |
| | | }, |
| | | { immediate: true }, |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <Grid class="w-full"> |
| | | <template #name="{ row }"> |
| | | <Input |
| | | v-model:value="row.name" |
| | | :disabled="disabled" |
| | | placeholder="请è¾å
¥å§å" |
| | | @change="() => recalc()" |
| | | /> |
| | | </template> |
| | | <template #contactPhone="{ row }"> |
| | | <Input |
| | | v-model:value="row.contactPhone" |
| | | :disabled="disabled" |
| | | :maxlength="11" |
| | | placeholder="11 使æºå·ï¼éå¡«ï¼" |
| | | @change="() => recalc()" |
| | | /> |
| | | </template> |
| | | <template #workUnit="{ row }"> |
| | | <Input |
| | | v-model:value="row.workUnit" |
| | | :disabled="disabled" |
| | | placeholder="请è¾å
¥æå±åä½" |
| | | @change="() => recalc()" |
| | | /> |
| | | </template> |
| | | <template #jobTitle="{ row }"> |
| | | <Input |
| | | v-model:value="row.jobTitle" |
| | | :disabled="disabled" |
| | | placeholder="请è¾å
¥èç§°/èå¡" |
| | | @change="() => recalc()" |
| | | /> |
| | | </template> |
| | | <template #memberRole="{ row }"> |
| | | <Select |
| | | v-model:value="row.memberRole" |
| | | :disabled="disabled" |
| | | :options="MEMBER_ROLE_OPTIONS" |
| | | placeholder="è¯·éæ©è§è²" |
| | | allow-clear |
| | | @change="() => recalc()" |
| | | /> |
| | | </template> |
| | | <template #sortNo="{ row }"> |
| | | <InputNumber |
| | | v-model:value="row.sortNo" |
| | | :disabled="disabled" |
| | | :min="1" |
| | | :precision="0" |
| | | class="!w-full" |
| | | @change="() => recalc()" |
| | | /> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.delete'), |
| | | danger: true, |
| | | type: 'link', |
| | | icon: ACTION_ICON.DELETE, |
| | | disabled, |
| | | onClick: handleDelete.bind(null, row), |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | <div v-if="!disabled" class="-mt-4 flex justify-center"> |
| | | <Button type="primary" ghost @click="handleAdd"> æ·»å æå </Button> |
| | | </div> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { UmsMemberProfileApi } from '#/api/ums/member/profile'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | approveProfileChange, |
| | | getProfileChangePage, |
| | | } from '#/api/ums/member/profile'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import RejectModal from './modules/reject-modal.vue'; |
| | | |
| | | defineOptions({ name: 'UmsMemberProfileChange' }); |
| | | |
| | | /** å®¡æ ¸ç¶æ */ |
| | | const ChangeStatus = { |
| | | PENDING: 0, |
| | | APPROVED: 10, |
| | | REJECTED: 20, |
| | | } as const; |
| | | |
| | | const statusOptions = [ |
| | | { label: 'å¾
å®¡æ ¸', value: ChangeStatus.PENDING }, |
| | | { label: 'å·²éè¿', value: ChangeStatus.APPROVED }, |
| | | { label: '已驳å', value: ChangeStatus.REJECTED }, |
| | | ]; |
| | | |
| | | const rejectModalRef = ref<InstanceType<typeof RejectModal>>(); |
| | | |
| | | function useSearchSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'memberNo', |
| | | label: 'ä¼åç¼å·', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥ä¼åç¼å·', allowClear: true }, |
| | | }, |
| | | { |
| | | fieldName: 'memberName', |
| | | label: 'ä¼ååç§°', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥ä¼ååç§°', allowClear: true }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'å®¡æ ¸ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: statusOptions, |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å®¡æ ¸éè¿ */ |
| | | async function handleApprove(row: UmsMemberProfileApi.ChangeRecord) { |
| | | await approveProfileChange(row.id!); |
| | | message.success('å®¡æ ¸éè¿ï¼èµæå·²æ´æ°å°ä¼åä¿¡æ¯'); |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** 驳å */ |
| | | function handleReject(row: UmsMemberProfileApi.ChangeRecord) { |
| | | rejectModalRef.value?.show(row.id!); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useSearchSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: [ |
| | | { field: 'memberNo', title: 'ä¼åç¼å·', width: 180, fixed: 'left' }, |
| | | { field: 'memberName', title: 'ä¼ååç§°', minWidth: 130 }, |
| | | { |
| | | field: 'status', |
| | | title: 'å®¡æ ¸ç¶æ', |
| | | width: 100, |
| | | slots: { default: 'status' }, |
| | | }, |
| | | { |
| | | field: 'changeSummary', |
| | | title: 'åæ´å
容', |
| | | minWidth: 360, |
| | | showOverflow: 'tooltip', |
| | | }, |
| | | { field: 'contactPhone', title: 'èç³»çµè¯', minWidth: 130 }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'æäº¤æ¶é´', |
| | | width: 165, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | field: 'auditTime', |
| | | title: 'å®¡æ ¸æ¶é´', |
| | | width: 165, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | field: 'auditRemark', |
| | | title: 'å®¡æ ¸æè§', |
| | | minWidth: 180, |
| | | showOverflow: 'tooltip', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 150, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ], |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getProfileChangePage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { keyField: 'id', isHover: true }, |
| | | toolbarConfig: { refresh: true, search: true }, |
| | | } as VxeTableGridOptions<UmsMemberProfileApi.ChangeRecord>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <RejectModal ref="rejectModalRef" @success="gridApi.query()" /> |
| | | <Grid table-title="ä¼åèµæåæ´å®¡æ ¸"> |
| | | <template #status="{ row }"> |
| | | <Tag v-if="row.status === ChangeStatus.PENDING" color="processing"> |
| | | å¾
å®¡æ ¸ |
| | | </Tag> |
| | | <Tag v-else-if="row.status === ChangeStatus.APPROVED" color="success"> |
| | | å·²éè¿ |
| | | </Tag> |
| | | <Tag v-else color="error">已驳å</Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'éè¿', |
| | | type: 'link', |
| | | auth: ['ums:member:change:audit'], |
| | | ifShow: () => row.status === ChangeStatus.PENDING, |
| | | popConfirm: { |
| | | title: `确认éè¿ä¼å ${row.memberName} çèµæåæ´åï¼éè¿åå°ç´æ¥æ´æ°ä¼åèµæ`, |
| | | confirm: handleApprove.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: '驳å', |
| | | type: 'link', |
| | | danger: true, |
| | | auth: ['ums:member:change:audit'], |
| | | ifShow: () => row.status === ChangeStatus.PENDING, |
| | | onClick: handleReject.bind(null, row), |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #empty>ææ èµæåæ´ç³è¯·</template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import { ref } from 'vue'; |
| | | |
| | | import { message, Modal, Textarea } from 'ant-design-vue'; |
| | | |
| | | import { rejectProfileChange } from '#/api/ums/member/profile'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const open = ref(false); |
| | | const submitting = ref(false); |
| | | const changeId = ref<number>(); |
| | | const remark = ref(''); |
| | | |
| | | function show(id: number) { |
| | | changeId.value = id; |
| | | remark.value = ''; |
| | | open.value = true; |
| | | } |
| | | |
| | | async function handleOk() { |
| | | if (!remark.value.trim()) { |
| | | message.warning('请填å驳ååå '); |
| | | return; |
| | | } |
| | | if (!changeId.value) { |
| | | return; |
| | | } |
| | | submitting.value = true; |
| | | try { |
| | | await rejectProfileChange(changeId.value, remark.value.trim()); |
| | | message.success('已驳åè¯¥èµæåæ´ç³è¯·'); |
| | | open.value = false; |
| | | emit('success'); |
| | | } finally { |
| | | submitting.value = false; |
| | | } |
| | | } |
| | | |
| | | defineExpose({ show }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | v-model:open="open" |
| | | title="驳åèµæåæ´ç³è¯·" |
| | | :confirm-loading="submitting" |
| | | ok-text="确认驳å" |
| | | cancel-text="åæ¶" |
| | | @ok="handleOk" |
| | | > |
| | | <div class="mb-2 text-sm text-gray-500"> |
| | | 驳ååå ä¼å±ç¤ºç»ä¼åï¼è¯·è¯´æå
·ä½éè¦ä¿®æ¹çå
容ã |
| | | </div> |
| | | <Textarea |
| | | v-model:value="remark" |
| | | :rows="4" |
| | | :maxlength="200" |
| | | show-count |
| | | placeholder="请è¾å
¥é©³ååå ï¼ä¾å¦ï¼èç³»çµè¯æ ¼å¼ä¸æ£ç¡®ï¼è¯·å¡«å 11 使æºå·" |
| | | /> |
| | | </Modal> |
| | | </template> |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'feePaid', |
| | | label: 'æ¯å¦ç¼´çº³ä¼è´¹', |
| | | component: 'Switch', |
| | | componentProps: (_values, form) => ({ |
| | | checkedChildren: '已缴', |
| | | unCheckedChildren: 'æªç¼´', |
| | | disabled: formType === 'detail', |
| | | onChange: (checked: boolean) => { |
| | | // åæ¶ç¼´çº³æ¶æ¸
空年度ï¼å¾éæ¶é»è®¤å½å年度ï¼ä»å¯ä¿®æ¹ |
| | | form.setFieldValue( |
| | | 'feeYear', |
| | | checked ? new Date().getFullYear() : undefined, |
| | | ); |
| | | }, |
| | | }), |
| | | }, |
| | | { |
| | | fieldName: 'feeYear', |
| | | label: '缴费年度', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ç¼´è´¹å¹´åº¦ï¼å¦ 2026', |
| | | min: 2000, |
| | | max: 2100, |
| | | precision: 0, |
| | | class: '!w-full', |
| | | disabled: formType === 'detail', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['feePaid'], |
| | | show: (values) => !!values.feePaid, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'feePaid', |
| | | title: 'ä¼è´¹ç¼´çº³', |
| | | minWidth: 120, |
| | | slots: { default: 'feePaid' }, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | minWidth: 85, |
| | |
| | | <template #memberGrade="{ row }"> |
| | | <span>{{ getGradeLabel(row) }}</span> |
| | | </template> |
| | | <template #feePaid="{ row }"> |
| | | <Tag v-if="row.feePaid" color="success"> |
| | | 已缴{{ row.feeYear ? `ï¼${row.feeYear} 年度ï¼` : '' }} |
| | | </Tag> |
| | | <Tag v-else color="default">æªç¼´</Tag> |
| | | </template> |
| | | <template #hasAttachment="{ row }"> |
| | | <Tag :color="row.hasAttachment ? 'success' : 'default'"> |
| | | {{ row.hasAttachment ? 'æ' : 'æ ' }} |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { markRaw } from 'vue'; |
| | | |
| | | import { AreaCascader } from '#/components/area'; |
| | | |
| | | /** ä¼åç±»å */ |
| | | const MEMBER_TYPE_UNIT = 2; |
| | | |
| | | /** æçèµæè¡¨åï¼ä»
å
许维æ¤èç³»æ¹å¼çèµæåæ®µï¼ */ |
| | | export function useProfileSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'no', |
| | | label: 'ä¼åç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | disabled: true, |
| | | placeholder: 'ç³»ç»èªå¨çæ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'name', |
| | | label: 'å§å/åä½åç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | disabled: true, |
| | | }, |
| | | }, |
| | | { |
| | | // ä»
ç¨äºæ§å¶åä½/ä¸ªäººåæ®µæ¾ç¤ºï¼ä¸åä¸æäº¤ |
| | | fieldName: 'memberType', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'contactUser', |
| | | label: 'è系人', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥è系人ï¼åä½ä¼åå¡«åï¼', |
| | | maxlength: 64, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['memberType'], |
| | | show: (values) => values.memberType === MEMBER_TYPE_UNIT, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'contactPhone', |
| | | label: 'èç³»çµè¯', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ 11 使æºå·', |
| | | maxlength: 20, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'contactEmail', |
| | | label: 'é®ç®±', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥é®ç®±', |
| | | maxlength: 128, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'workUnit', |
| | | label: 'å·¥ä½åä½', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å·¥ä½åä½', |
| | | maxlength: 128, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['memberType'], |
| | | show: (values) => values.memberType !== MEMBER_TYPE_UNIT, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'title', |
| | | label: 'èç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥èç§°', |
| | | maxlength: 64, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['memberType'], |
| | | show: (values) => values.memberType !== MEMBER_TYPE_UNIT, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'legalPerson', |
| | | label: 'æ³å®ä»£è¡¨äºº', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ³å®ä»£è¡¨äºº', |
| | | maxlength: 64, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['memberType'], |
| | | show: (values) => values.memberType === MEMBER_TYPE_UNIT, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'organizationCode', |
| | | label: 'ç»ä¸ç¤¾ä¼ä¿¡ç¨ä»£ç ', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ç»ä¸ç¤¾ä¼ä¿¡ç¨ä»£ç ', |
| | | maxlength: 64, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['memberType'], |
| | | show: (values) => values.memberType === MEMBER_TYPE_UNIT, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'researchDirection', |
| | | label: 'ç ç©¶æ¹å', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ç ç©¶æ¹å', |
| | | autoSize: { minRows: 2, maxRows: 3 }, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'unitIntro', |
| | | label: 'åä½ç®ä»', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥åä½ç®ä»', |
| | | autoSize: { minRows: 2, maxRows: 3 }, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['memberType'], |
| | | show: (values) => values.memberType === MEMBER_TYPE_UNIT, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'regionCode', |
| | | label: 'å°åº', |
| | | component: markRaw(AreaCascader), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©å°åº', |
| | | allowClear: true, |
| | | class: '!w-full', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'industry', |
| | | label: 'è¡ä¸', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.UMS_INDUSTRY, 'string'), |
| | | placeholder: 'è¯·éæ©è¡ä¸', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { UmsMemberProfileApi } from '#/api/ums/member/profile'; |
| | | |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | |
| | | import { Alert, Button, message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | applyProfileChange, |
| | | getMyLatestChange, |
| | | getMyProfile, |
| | | } from '#/api/ums/member/profile'; |
| | | |
| | | import { useProfileSchema } from './data'; |
| | | |
| | | defineOptions({ name: 'UmsMemberProfile' }); |
| | | |
| | | const loading = ref(false); |
| | | const submitting = ref(false); |
| | | const latestChange = ref<UmsMemberProfileApi.ChangeRecord>(); |
| | | |
| | | /** æè¿ä¸æ¬¡ç³è¯·å¾
å®¡æ ¸æ¶ç¦æ¢éå¤æäº¤ */ |
| | | const pending = computed(() => latestChange.value?.status === 0); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | labelWidth: 130, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'vertical', |
| | | schema: useProfileSchema(), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | /** åªæäº¤å
许ä¼åç»´æ¤çåæ®µ */ |
| | | async function buildSubmitData(): Promise<UmsMemberProfileApi.ApplyReq> { |
| | | const values = (await formApi.getValues()) as UmsMemberProfileApi.ApplyReq; |
| | | return { |
| | | contactUser: values.contactUser, |
| | | contactPhone: values.contactPhone, |
| | | contactEmail: values.contactEmail, |
| | | workUnit: values.workUnit, |
| | | title: values.title, |
| | | researchDirection: values.researchDirection, |
| | | unitIntro: values.unitIntro, |
| | | legalPerson: values.legalPerson, |
| | | organizationCode: values.organizationCode, |
| | | regionCode: values.regionCode, |
| | | industry: values.industry, |
| | | }; |
| | | } |
| | | |
| | | async function loadData() { |
| | | loading.value = true; |
| | | try { |
| | | const [profile, latest] = await Promise.all([ |
| | | getMyProfile(), |
| | | getMyLatestChange(), |
| | | ]); |
| | | latestChange.value = latest ?? undefined; |
| | | await formApi.setValues( |
| | | { |
| | | no: profile.no, |
| | | name: profile.name, |
| | | memberType: profile.memberType, |
| | | contactUser: profile.contactUser, |
| | | contactPhone: profile.contactPhone, |
| | | contactEmail: profile.contactEmail, |
| | | workUnit: profile.workUnit, |
| | | title: profile.title, |
| | | researchDirection: profile.researchDirection, |
| | | unitIntro: profile.unitIntro, |
| | | legalPerson: profile.legalPerson, |
| | | organizationCode: profile.organizationCode, |
| | | regionCode: profile.regionCode, |
| | | industry: profile.industry, |
| | | }, |
| | | false, |
| | | ); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function handleSubmit() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | submitting.value = true; |
| | | try { |
| | | await applyProfileChange(await buildSubmitData()); |
| | | message.success('èµæåæ´ç³è¯·å·²æäº¤ï¼çå¾
管çåå®¡æ ¸'); |
| | | await loadData(); |
| | | } finally { |
| | | submitting.value = false; |
| | | } |
| | | } |
| | | |
| | | const latestAlert = computed(() => { |
| | | const latest = latestChange.value; |
| | | if (!latest) { |
| | | return null; |
| | | } |
| | | if (latest.status === 0) { |
| | | return { |
| | | type: 'info' as const, |
| | | message: 'èµæåæ´ç³è¯·å®¡æ ¸ä¸', |
| | | description: `æäº¤æ¶é´ï¼${latest.createTime || '-'}ï¼è¯·çå¾
管çåå®¡æ ¸ã`, |
| | | }; |
| | | } |
| | | if (latest.status === 10) { |
| | | return { |
| | | type: 'success' as const, |
| | | message: 'æè¿ä¸æ¬¡èµæåæ´å·²éè¿', |
| | | description: `å®¡æ ¸æ¶é´ï¼${latest.auditTime || '-'}`, |
| | | }; |
| | | } |
| | | return { |
| | | type: 'error' as const, |
| | | message: 'æè¿ä¸æ¬¡èµæåæ´è¢«é©³å', |
| | | description: `驳ååå ï¼${latest.auditRemark || '-'}ï¼å®¡æ ¸æ¶é´ï¼${latest.auditTime || '-'}`, |
| | | }; |
| | | }); |
| | | |
| | | onMounted(loadData); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <div class="mx-auto max-w-5xl p-4"> |
| | | <div class="mb-4 rounded-lg border border-border bg-background p-4"> |
| | | <div class="mb-1 text-base font-semibold">æçèµæ</div> |
| | | <div class="text-sm text-gray-500"> |
| | | ä¿®æ¹èç³»æ¹å¼çèµæåæäº¤ï¼ç®¡çåå®¡æ ¸éè¿åçæï¼å®¡æ ¸æé´ä¸å¯éå¤æäº¤ã |
| | | </div> |
| | | </div> |
| | | |
| | | <Alert |
| | | v-if="latestAlert" |
| | | class="mb-4" |
| | | :type="latestAlert.type" |
| | | show-icon |
| | | :message="latestAlert.message" |
| | | :description="latestAlert.description" |
| | | /> |
| | | |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <Form class="mx-3" /> |
| | | <div class="mt-4 flex justify-end"> |
| | | <Button |
| | | type="primary" |
| | | :loading="submitting || loading" |
| | | :disabled="pending" |
| | | @click="handleSubmit" |
| | | > |
| | | {{ pending ? 'å®¡æ ¸ä¸ï¼æä¸å¯æäº¤' : 'æäº¤å®¡æ ¸' }} |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import { VehicleApplicationApi } from '#/api/vehicle/application'; |
| | | |
| | | /** ç¨è½¦ç³è¯·ç¶æé项 */ |
| | | export const StatusOptions = [ |
| | | { label: 'è稿', value: VehicleApplicationApi.Status.DRAFT }, |
| | | { label: '审æ¹ä¸', value: VehicleApplicationApi.Status.APPROVING }, |
| | | { label: 'å·²éè¿', value: VehicleApplicationApi.Status.APPROVED }, |
| | | { label: '已驳å', value: VehicleApplicationApi.Status.REJECTED }, |
| | | ]; |
| | | |
| | | export function getStatusName(status?: number) { |
| | | return StatusOptions.find((item) => item.value === status)?.label ?? '-'; |
| | | } |
| | | |
| | | /** æ°å¢/ä¿®æ¹è¡¨å */ |
| | | export function useFormSchema(formType: 'create' | 'edit' | 'detail') { |
| | | const disabled = formType === 'detail'; |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'useStartTime', |
| | | label: 'ç¨è½¦å¼å§æ¶é´', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | showTime: true, |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | placeholder: 'è¯·éæ©ç¨è½¦å¼å§æ¶é´', |
| | | style: { width: '100%' }, |
| | | disabled, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'useEndTime', |
| | | label: 'ç¨è½¦ç»ææ¶é´', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | showTime: true, |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | placeholder: 'è¯·éæ©ç¨è½¦ç»ææ¶é´', |
| | | style: { width: '100%' }, |
| | | disabled, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'departure', |
| | | label: 'åºåå°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥åºåå°', |
| | | maxlength: 128, |
| | | disabled, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'destination', |
| | | label: 'ç®çå°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ç®çå°', |
| | | maxlength: 128, |
| | | disabled, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'passengers', |
| | | label: 'éè¡äººæ°', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | min: 1, |
| | | precision: 0, |
| | | placeholder: '请è¾å
¥éè¡äººæ°', |
| | | class: '!w-full', |
| | | disabled, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'vehicleNo', |
| | | label: '车çå·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'éå¡«ï¼å¦ èF12345', |
| | | maxlength: 32, |
| | | disabled, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'reason', |
| | | label: 'ç¨è½¦äºç±', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ç¨è½¦äºç±', |
| | | autoSize: { minRows: 2, maxRows: 4 }, |
| | | disabled, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | autoSize: { minRows: 2, maxRows: 3 }, |
| | | disabled, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ] as VbenFormSchema[]; |
| | | } |
| | | |
| | | /** å表æç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'no', |
| | | label: 'ç³è¯·åå·', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥ç³è¯·åå·', allowClear: true }, |
| | | }, |
| | | { |
| | | fieldName: 'applicantName', |
| | | label: 'ç³è¯·äºº', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥ç³è¯·äºº', allowClear: true }, |
| | | }, |
| | | { |
| | | fieldName: 'destination', |
| | | label: 'ç®çå°', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥ç®çå°', allowClear: true }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: StatusOptions, |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表å */ |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { field: 'no', title: 'ç³è¯·åå·', width: 175, fixed: 'left' }, |
| | | { field: 'applicantName', title: 'ç³è¯·äºº', width: 110 }, |
| | | { |
| | | field: 'useStartTime', |
| | | title: 'ç¨è½¦å¼å§', |
| | | width: 165, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | field: 'useEndTime', |
| | | title: 'ç¨è½¦ç»æ', |
| | | width: 165, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { field: 'departure', title: 'åºåå°', minWidth: 140 }, |
| | | { field: 'destination', title: 'ç®çå°', minWidth: 140 }, |
| | | { field: 'passengers', title: 'éè¡äººæ°', width: 95 }, |
| | | { field: 'reason', title: 'ç¨è½¦äºç±', minWidth: 180, showOverflow: 'tooltip' }, |
| | | { field: 'vehicleNo', title: '车çå·', width: 110 }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 100, |
| | | slots: { default: 'status' }, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 220, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { VehicleApplicationApi } from '#/api/vehicle/application'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { isEmpty } from '@vben/utils'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteVehicleApplication, |
| | | getVehicleApplicationPage, |
| | | getVehicleApproveProcessList, |
| | | } from '#/api/vehicle/application'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import ProcessSelectModal from './modules/process-select-modal.vue'; |
| | | |
| | | defineOptions({ name: 'VehicleApplication' }); |
| | | |
| | | const router = useRouter(); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ProcessModal, processModalApi] = useVbenModal({ |
| | | connectedComponent: ProcessSelectModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | function handleEdit(row: VehicleApplicationApi.Application) { |
| | | formModalApi.setData({ formType: 'edit', id: row.id }).open(); |
| | | } |
| | | |
| | | function handleDetail(row: VehicleApplicationApi.Application) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | async function handleDelete(ids: number[]) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting'), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await Promise.all(ids.map((id) => deleteVehicleApplication(id))); |
| | | message.success($t('ui.actionMessage.deleteSuccess')); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | async function handleSubmit(row: VehicleApplicationApi.Application) { |
| | | const hideLoading = message.loading({ |
| | | content: 'è·åå®¡æ¹æµç¨...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | const processList = await getVehicleApproveProcessList(); |
| | | hideLoading(); |
| | | if (!processList || processList.length === 0) { |
| | | message.warning( |
| | | 'ææ å¯ç¨ç¨è½¦å®¡æ¹æµç¨ï¼è¯·å
å° BPM æµç¨è®¾è®¡å¨åå¸ãç¨è½¦å®¡æ¹ãæµç¨', |
| | | ); |
| | | return; |
| | | } |
| | | processModalApi.setData({ id: row.id, processList }).open(); |
| | | } catch { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | function handleProcessDetail(row: VehicleApplicationApi.Application) { |
| | | if (!row.processInstanceId) { |
| | | message.warning('该ç¨è½¦ç³è¯·å°æªæäº¤å®¡æ¹'); |
| | | return; |
| | | } |
| | | router.push({ |
| | | name: 'BpmProcessInstanceDetail', |
| | | query: { id: row.processInstanceId }, |
| | | }); |
| | | } |
| | | |
| | | const checkedIds = ref<number[]>([]); |
| | | function handleRowCheckboxChange({ |
| | | records, |
| | | }: { |
| | | records: VehicleApplicationApi.Application[]; |
| | | }) { |
| | | checkedIds.value = records.map((item) => item.id!); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getVehicleApplicationPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { keyField: 'id', isHover: true }, |
| | | toolbarConfig: { refresh: true, search: true }, |
| | | } as VxeTableGridOptions<VehicleApplicationApi.Application>, |
| | | gridEvents: { |
| | | checkboxAll: handleRowCheckboxChange, |
| | | checkboxChange: handleRowCheckboxChange, |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ProcessModal @success="handleRefresh" /> |
| | | <Grid table-title="ç¨è½¦ç³è¯·"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'æ°å¢ç¨è½¦ç³è¯·', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['vehicle:application:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: 'æ¹éå é¤', |
| | | type: 'primary', |
| | | danger: true, |
| | | disabled: isEmpty(checkedIds), |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['vehicle:application:delete'], |
| | | popConfirm: { |
| | | title: 'æ¯å¦å 餿éç¨è½¦ç³è¯·ï¼ï¼ä»
èç¨¿ç¶æå¯å é¤ï¼', |
| | | confirm: handleDelete.bind(null, checkedIds), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #status="{ row }"> |
| | | <Tag v-if="row.status === 0" color="default">è稿</Tag> |
| | | <Tag v-else-if="row.status === 15" color="processing">审æ¹ä¸</Tag> |
| | | <Tag v-else-if="row.status === 20" color="success">å·²éè¿</Tag> |
| | | <Tag v-else color="error">已驳å</Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.detail'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['vehicle:application:query'], |
| | | onClick: handleDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['vehicle:application:update'], |
| | | ifShow: () => row.status === 0 || row.status === 35, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æäº¤å®¡æ¹', |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['vehicle:application:submit'], |
| | | ifShow: () => row.status === 0 || row.status === 35, |
| | | onClick: handleSubmit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æ¥ç审æ¹', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['vehicle:application:query'], |
| | | ifShow: () => !!row.processInstanceId, |
| | | onClick: handleProcessDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['vehicle:application:delete'], |
| | | ifShow: () => row.status === 0, |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.no]), |
| | | confirm: handleDelete.bind(null, [row.id!]), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VehicleApplicationApi } from '#/api/vehicle/application'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { $t } from '@vben/locales'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createVehicleApplication, |
| | | getVehicleApplication, |
| | | updateVehicleApplication, |
| | | } from '#/api/vehicle/application'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | type FormType = 'create' | 'detail' | 'edit'; |
| | | |
| | | const formType = ref<FormType>('create'); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | labelWidth: 130, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'vertical', |
| | | schema: useFormSchema('create'), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = |
| | | (await formApi.getValues()) as VehicleApplicationApi.Application; |
| | | try { |
| | | await (formType.value === 'create' |
| | | ? createVehicleApplication(data) |
| | | : updateVehicleApplication(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | formApi.setDisabled(data.formType === 'detail'); |
| | | formApi.updateSchema(useFormSchema(data.formType)); |
| | | if (!data.id) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | const application = await getVehicleApplication(data.id); |
| | | await formApi.setValues(application, false); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | :title=" |
| | | formType === 'create' |
| | | ? $t('ui.actionTitle.create', ['ç¨è½¦ç³è¯·']) |
| | | : formType === 'edit' |
| | | ? $t('ui.actionTitle.edit', ['ç¨è½¦ç³è¯·']) |
| | | : 'ç¨è½¦ç³è¯·è¯¦æ
' |
| | | " |
| | | class="w-3/4" |
| | | :close-on-click-modal="false" |
| | | :show-confirm-button="formType !== 'detail'" |
| | | > |
| | | <Form class="mx-3" /> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VehicleApplicationApi } from '#/api/vehicle/application'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message, Select } from 'ant-design-vue'; |
| | | |
| | | import { submitVehicleApplication } from '#/api/vehicle/application'; |
| | | |
| | | defineOptions({ name: 'VehicleApplicationProcessSelect' }); |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const applicationId = ref<number>(); |
| | | const processList = ref<VehicleApplicationApi.ProcessDefinition[]>([]); |
| | | const selectedProcessKey = ref(''); |
| | | |
| | | const processOptions = computed(() => |
| | | processList.value.map((item) => ({ label: item.name, value: item.key })), |
| | | ); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | if (!selectedProcessKey.value) { |
| | | message.warning('è¯·éæ©å®¡æ¹æµç¨'); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | await submitVehicleApplication( |
| | | applicationId.value!, |
| | | selectedProcessKey.value, |
| | | ); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success('æäº¤æåï¼çå¾
é级审æ¹'); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | applicationId.value = undefined; |
| | | selectedProcessKey.value = ''; |
| | | processList.value = []; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ |
| | | id: number; |
| | | processList: VehicleApplicationApi.ProcessDefinition[]; |
| | | }>(); |
| | | applicationId.value = data.id; |
| | | processList.value = data.processList || []; |
| | | if (processList.value.length > 0) { |
| | | selectedProcessKey.value = processList.value[0]!.key; |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="éæ©ç¨è½¦å®¡æ¹æµç¨" class="w-1/3"> |
| | | <div class="mb-4"> |
| | | <span class="mr-2">å®¡æ¹æµç¨ï¼</span> |
| | | <Select |
| | | v-model:value="selectedProcessKey" |
| | | :options="processOptions" |
| | | style="width: 240px" |
| | | placeholder="è¯·éæ©å®¡æ¹æµç¨" |
| | | /> |
| | | </div> |
| | | <p class="text-sm text-gray-500"> |
| | | æäº¤åæ BPM ä¸é
ç½®çç¨è½¦å®¡æ ¸/é级审æ¹èç¹æµè½¬ï¼å®¡æ¹ç»æä¼ååå°æ¬ç³è¯·å¹¶åé¦ç»ç³è¯·äººã |
| | | </p> |
| | | </Modal> |
| | | </template> |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { WxCpActivityApi } from '#/api/wxcp/activity'; |
| | | |
| | | import { ACTIVITY_TYPE } from '#/api/wxcp/activity'; |
| | | |
| | | import { |
| | | ACTIVITY_STATUS_OPTIONS, |
| | | PUSH_SCOPE_OPTIONS, |
| | |
| | | /** 表åç±»å */ |
| | | export type ActivityFormType = 'create' | 'update' | 'detail'; |
| | | |
| | | /** æ´»å¨æ¨¡æ¿ç±»åé项 */ |
| | | export const ACTIVITY_TYPE_OPTIONS = [ |
| | | { |
| | | label: '纯éç¥ï¼åªæ¨ä¸æ¡ææ¬ï¼', |
| | | value: ACTIVITY_TYPE.TEXT, |
| | | }, |
| | | { |
| | | label: 'åæ¡æ¶æ¯ï¼å¯ç¹å»å¡çï¼', |
| | | value: ACTIVITY_TYPE.MESSAGE, |
| | | }, |
| | | { |
| | | label: 'å¾çéç¥ï¼ä¸ä¼ å¾çæ¨éï¼', |
| | | value: ACTIVITY_TYPE.IMAGE, |
| | | }, |
| | | { |
| | | label: 'éä»¶éç¥ï¼æ¨éæä»¶ä¾ä¸è½½ï¼', |
| | | value: ACTIVITY_TYPE.FILE, |
| | | }, |
| | | { |
| | | label: 'æ°æ®éé/è¯ä»·ï¼å¡«å表åï¼', |
| | | value: ACTIVITY_TYPE.FORM, |
| | | }, |
| | | ]; |
| | | |
| | | /** ééåæ®µç±»åé项 */ |
| | | export const FIELD_TYPE_OPTIONS = [ |
| | | { label: 'åè¡ææ¬', value: 'text' }, |
| | | { label: 'å¤è¡ææ¬', value: 'textarea' }, |
| | | { label: 'æ°å', value: 'number' }, |
| | | { label: 'æ¥æ', value: 'date' }, |
| | | { label: '䏿鿩', value: 'select' }, |
| | | { label: 'åé', value: 'radio' }, |
| | | { label: 'å¤é', value: 'checkbox' }, |
| | | { label: 'è¯å', value: 'rating' }, |
| | | ]; |
| | | |
| | | const DATETIME_PROPS = { |
| | | showTime: true, |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | } as const; |
| | | |
| | | /** ä»
æ°æ®é鿍¡æ¿éè¦æ¥åæ¶é´ä¸åé¢ */ |
| | | function isFormType(values: Record<string, unknown>) { |
| | | return values.activityType === ACTIVITY_TYPE.FORM; |
| | | } |
| | | |
| | | /** æ´»å¨è¡¨å */ |
| | | export function useActivityFormSchema( |
| | |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'activityType', |
| | | label: 'æ´»å¨æ¨¡æ¿', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: ACTIVITY_TYPE_OPTIONS, |
| | | placeholder: 'è¯·éæ©æ´»å¨æ¨¡æ¿', |
| | | disabled: readonly, |
| | | }, |
| | | rules: 'required', |
| | | help: 'ä¸å模æ¿å³å®æ¨éå½¢æï¼çº¯éç¥/åæ¡æ¶æ¯/å¾ç/éä»¶ï¼æå¸¦è¡¨åçæ°æ®éé', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'title', |
| | | label: 'æ´»å¨æ é¢', |
| | | component: 'Input', |
| | |
| | | disabled: readonly, |
| | | }, |
| | | rules: 'required', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'buttonText', |
| | | label: 'æé®ææ¡', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'å¦ï¼æ¥ç详æ
/ ç«å³æ¥å', |
| | | disabled: readonly, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['activityType'], |
| | | show: (values) => |
| | | values.activityType === ACTIVITY_TYPE.MESSAGE || |
| | | values.activityType === ACTIVITY_TYPE.FORM, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | |
| | | label: 'æ¥åå¼å§', |
| | | component: 'DatePicker', |
| | | componentProps: { ...DATETIME_PROPS, disabled: readonly }, |
| | | dependencies: { |
| | | triggerFields: ['activityType'], |
| | | show: isFormType, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'signupEndTime', |
| | | label: 'æ¥åæªæ¢', |
| | | component: 'DatePicker', |
| | | componentProps: { ...DATETIME_PROPS, disabled: readonly }, |
| | | dependencies: { |
| | | triggerFields: ['activityType'], |
| | | show: isFormType, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'signupLimit', |
| | |
| | | class: 'w-full', |
| | | disabled: readonly, |
| | | }, |
| | | defaultValue: 0, |
| | | rules: 'required', |
| | | help: 'å¡« 0 表示ä¸éåé¢', |
| | | dependencies: { |
| | | triggerFields: ['activityType'], |
| | | show: isFormType, |
| | | }, |
| | | }, |
| | | { |
| | | // å°é¢èµ°éä»¶ä¸é´è¡¨ï¼è¿éæäº¤ blobIdsï¼åæ¾è¯» attachmentListï¼è§ form.vue çæ å°å½æ°ï¼ |
| | |
| | | disabled: readonly, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | // 详æ
å¾çèµ°å¦ä¸å¥éä»¶å
³ç³»ï¼recordType = wxcp_activity_detailï¼ï¼ä¸å°é¢äºä¸å½±åã |
| | | // æ¥å页æå®æå¨ãæ´»å¨è¯¦æ
ãæå䏿¹ãæå徿¯ä¾å®æ´å±ç¤ºï¼ä¸è£åªï¼ï¼ |
| | | // æä»¥äºç»´ç /æµ·æ¥è¿ç±»è¢«è£å°±å¤±æçå¾å¿
é¡»æ¾è¿éï¼å«æ¾å°é¢ã |
| | | fieldName: 'detailBlobIds', |
| | | label: '详æ
å¾ç', |
| | | component: 'ImageUpload', |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | resultField: 'id', |
| | | maxNumber: 5, |
| | | multiple: true, |
| | | disabled: readonly, |
| | | }, |
| | | help: 'éå¡«ï¼æå¤ 5 å¼ ï¼æ´»å¨é¡µæå徿¯ä¾å®æ´æ¾ç¤ºä¸è£åªï¼äºç»´ç 建议æ¾è¿é', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | // å¾çéç¥æ¨¡æ¿ï¼æ¨éæ¶éå¼ åå¾çæ¶æ¯ï¼ä¼ä¸å¾®ä¿¡å¾çæ¶æ¯ï¼ |
| | | fieldName: 'imageBlobIds', |
| | | label: 'æ¨éå¾ç', |
| | | component: 'ImageUpload', |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | resultField: 'id', |
| | | maxNumber: 9, |
| | | multiple: true, |
| | | disabled: readonly, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['activityType'], |
| | | show: (values) => values.activityType === ACTIVITY_TYPE.IMAGE, |
| | | }, |
| | | help: 'æ¨éæ¶éå¼ åéå¾çæ¶æ¯ï¼æå¤ 9 å¼ ', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | // éä»¶éç¥æ¨¡æ¿ï¼æ¨éæ¶é个åæä»¶æ¶æ¯ï¼æ¥æ¶äººå¯ç´æ¥ä¸è½½ |
| | | fieldName: 'fileBlobIds', |
| | | label: 'æ¨ééä»¶', |
| | | component: 'FileUpload', |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | maxNumber: 5, |
| | | multiple: true, |
| | | showDescription: !readonly, |
| | | disabled: readonly, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['activityType'], |
| | | show: (values) => values.activityType === ACTIVITY_TYPE.FILE, |
| | | }, |
| | | help: 'æ¨éæ¶é个åéæä»¶æ¶æ¯ï¼å个ä¸è¶
è¿ 20MBï¼æå¤ 5 个', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'ratingEnabled', |
| | | label: 'å¼å¯è¯ä»·', |
| | | component: 'Switch', |
| | | componentProps: { disabled: readonly }, |
| | | dependencies: { |
| | | triggerFields: ['activityType'], |
| | | show: isFormType, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'ratingTitle', |
| | | label: 'è¯ä»·æ é¢', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'å¦ï¼æ´»å¨æ»¡æåº¦è¯ä»·', |
| | | disabled: readonly, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['activityType', 'ratingEnabled'], |
| | | show: (values) => isFormType(values) && values.ratingEnabled === true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'ratingMax', |
| | | label: 'æé«æçº§', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | min: 1, |
| | | max: 10, |
| | | class: 'w-full', |
| | | disabled: readonly, |
| | | }, |
| | | defaultValue: 5, |
| | | dependencies: { |
| | | triggerFields: ['activityType', 'ratingEnabled'], |
| | | show: (values) => isFormType(values) && values.ratingEnabled === true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | |
| | | /** æ´»å¨åè¡¨åæ®µ */ |
| | | export function useActivityGridColumns(): VxeTableGridOptions<WxCpActivityApi.Activity>['columns'] { |
| | | return [ |
| | | { |
| | | field: 'activityTypeName', |
| | | title: '模æ¿', |
| | | width: 150, |
| | | }, |
| | | { |
| | | field: 'title', |
| | | title: 'æ´»å¨æ é¢', |
| | |
| | | slots: { default: 'content' }, |
| | | }, |
| | | { |
| | | field: 'fieldValues', |
| | | title: 'ééå
容', |
| | | minWidth: 200, |
| | | slots: { default: 'fieldValues' }, |
| | | }, |
| | | { |
| | | field: 'ratingScore', |
| | | title: 'è¯ä»·', |
| | | width: 80, |
| | | slots: { default: 'ratingScore' }, |
| | | }, |
| | | { |
| | | field: 'attachment', |
| | | title: 'éä»¶', |
| | | width: 80, |
| | |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | ACTIVITY_TYPE, |
| | | cancelActivity, |
| | | deleteActivity, |
| | | finishActivity, |
| | |
| | | |
| | | async function handlePublish(row: WxCpActivityApi.Activity) { |
| | | await publishActivity(row.id!); |
| | | message.success('å叿åï¼ç°å¨å¯ä»¥æ¨éåçæäºç»´ç äº'); |
| | | message.success('å叿åï¼ç°å¨å¯ä»¥æ¨éäº'); |
| | | handleRefresh(); |
| | | } |
| | | |
| | |
| | | auth: ['wxcp:activity:update'], |
| | | ifShow: row.status === ACTIVITY_STATUS.DRAFT, |
| | | popConfirm: { |
| | | title: 'åå¸åæåå³å¯éè¿äºç»´ç æ¥åï¼ç¡®è®¤åå¸ï¼', |
| | | title: 'åå¸åå³å¯æ¨éï¼ç¡®è®¤åå¸ï¼', |
| | | confirm: handlePublish.bind(null, row), |
| | | }, |
| | | }, |
| | |
| | | { |
| | | label: 'äºç»´ç ', |
| | | type: 'link', |
| | | ifShow: row.status === ACTIVITY_STATUS.PUBLISHED, |
| | | // åªææ°æ®é鿍¡æ¿æææ¥å/å¡«å页ï¼å
¶å®æ¨¡æ¿æ¨éå³å®æ |
| | | ifShow: |
| | | row.status === ACTIVITY_STATUS.PUBLISHED && |
| | | row.activityType === ACTIVITY_TYPE.FORM, |
| | | onClick: () => qrCodeModalApi.setData({ activity: row }).open(), |
| | | }, |
| | | { |
| | | label: 'ç»è®¡', |
| | | type: 'link', |
| | | auth: ['wxcp:activity:query'], |
| | | ifShow: row.activityType === ACTIVITY_TYPE.FORM, |
| | | onClick: () => statsModalApi.setData({ activity: row }).open(), |
| | | }, |
| | | { |
| | | label: 'æ¥åè®°å½', |
| | | type: 'link', |
| | | auth: ['wxcp:signup:query'], |
| | | ifShow: row.activityType === ACTIVITY_TYPE.FORM, |
| | | onClick: () => signupModalApi.setData({ activity: row }).open(), |
| | | }, |
| | | ]" |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { WxCpActivityApi } from '#/api/wxcp/activity'; |
| | | |
| | | import { Button, Input, Select, Switch } from 'ant-design-vue'; |
| | | |
| | | import { FIELD_TYPE_OPTIONS } from '../data'; |
| | | |
| | | defineOptions({ name: 'WxCpActivityFieldEditor' }); |
| | | |
| | | interface Props { |
| | | /** ééåæ®µå表 */ |
| | | modelValue?: WxCpActivityApi.Field[]; |
| | | disabled?: boolean; |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<Props>(), { |
| | | modelValue: () => [], |
| | | disabled: false, |
| | | }); |
| | | |
| | | const emit = defineEmits<{ |
| | | 'update:modelValue': [value: WxCpActivityApi.Field[]]; |
| | | }>(); |
| | | |
| | | /** éè¦å¡«åé项çåæ®µç±»å */ |
| | | const OPTION_TYPES = new Set(['checkbox', 'radio', 'select']); |
| | | |
| | | function update(index: number, patch: Partial<WxCpActivityApi.Field>) { |
| | | const list = [...props.modelValue]; |
| | | list[index] = { ...list[index], ...patch }; |
| | | emit('update:modelValue', list); |
| | | } |
| | | |
| | | function handleAdd() { |
| | | emit('update:modelValue', [ |
| | | ...props.modelValue, |
| | | { |
| | | fieldLabel: '', |
| | | fieldType: 'text', |
| | | required: false, |
| | | sortNo: props.modelValue.length + 1, |
| | | }, |
| | | ]); |
| | | } |
| | | |
| | | function handleRemove(index: number) { |
| | | const list = props.modelValue.filter((_, i) => i !== index); |
| | | emit('update:modelValue', list); |
| | | } |
| | | |
| | | function needOptions(type?: string) { |
| | | return OPTION_TYPES.has(type ?? ''); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="mb-4"> |
| | | <div class="mb-2 flex items-center justify-between"> |
| | | <span class="text-sm font-medium">ééåæ®µ</span> |
| | | <Button size="small" :disabled="disabled" @click="handleAdd"> |
| | | æ·»å åæ®µ |
| | | </Button> |
| | | </div> |
| | | |
| | | <div v-if="!modelValue.length" class="text-xs text-gray-400"> |
| | | æªæ·»å åæ®µæ¶ï¼æååªéå¡«åå§åãææºå·ãåä¸è¯´æå¹¶ä¸ä¼ éä»¶ã |
| | | </div> |
| | | |
| | | <div |
| | | v-for="(item, index) in modelValue" |
| | | :key="index" |
| | | class="mb-2 rounded border border-gray-200 p-2 dark:border-gray-700" |
| | | > |
| | | <div class="flex flex-wrap items-center gap-2"> |
| | | <Input |
| | | :value="item.fieldLabel" |
| | | class="w-44" |
| | | placeholder="åæ®µåç§°" |
| | | :disabled="disabled" |
| | | @update:value="(value: string) => update(index, { fieldLabel: value })" |
| | | /> |
| | | <Select |
| | | :value="item.fieldType" |
| | | class="w-32" |
| | | :options="FIELD_TYPE_OPTIONS" |
| | | :disabled="disabled" |
| | | @update:value="(value: unknown) => update(index, { fieldType: String(value) })" |
| | | /> |
| | | <Input |
| | | v-if="needOptions(item.fieldType)" |
| | | :value="item.fieldOptions" |
| | | class="w-64" |
| | | placeholder="é项ï¼ç¨è±æéå·åé" |
| | | :disabled="disabled" |
| | | @update:value="(value: string) => update(index, { fieldOptions: value })" |
| | | /> |
| | | <Input |
| | | v-else |
| | | :value="item.placeholder" |
| | | class="w-64" |
| | | placeholder="è¾å
¥æç¤ºï¼éå¡«ï¼" |
| | | :disabled="disabled" |
| | | @update:value="(value: string) => update(index, { placeholder: value })" |
| | | /> |
| | | <span class="text-xs text-gray-500">å¿
å¡«</span> |
| | | <Switch |
| | | :checked="item.required" |
| | | size="small" |
| | | :disabled="disabled" |
| | | @change="(checked: unknown) => update(index, { required: checked === true })" |
| | | /> |
| | | <Button |
| | | danger |
| | | type="link" |
| | | size="small" |
| | | :disabled="disabled" |
| | | @click="handleRemove(index)" |
| | | > |
| | | å é¤ |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { toAccessibleFileUrl } from '#/utils'; |
| | | import { |
| | | ACTIVITY_TYPE, |
| | | createActivity, |
| | | getActivity, |
| | | updateActivity, |
| | | } from '#/api/wxcp/activity'; |
| | | |
| | | import { useActivityFormSchema } from '../data'; |
| | | import { FIELD_TYPE_OPTIONS, useActivityFormSchema } from '../data'; |
| | | import FieldEditor from './field-editor.vue'; |
| | | |
| | | defineOptions({ name: 'WxCpActivityForm' }); |
| | | |
| | |
| | | const formType = ref<ActivityFormType>('create'); |
| | | const id = ref<number>(); |
| | | const submitting = ref(false); |
| | | |
| | | /** å½åæ´»å¨æ¨¡æ¿ç±»åï¼ç¨äºæ§å¶ééåæ®µç¼è¾å¨çæ¾é */ |
| | | const activityType = ref<number>(ACTIVITY_TYPE.FORM); |
| | | /** æ°æ®é鿍¡æ¿çèªå®ä¹å段ï¼è¡¨å项ä¹å¤åç¬ç»´æ¤ï¼ */ |
| | | const fields = ref<WxCpActivityApi.Field[]>([]); |
| | | |
| | | const showFieldEditor = computed( |
| | | () => activityType.value === ACTIVITY_TYPE.FORM && formType.value !== 'detail', |
| | | ); |
| | | |
| | | function fieldTypeLabel(type?: string) { |
| | | return ( |
| | | FIELD_TYPE_OPTIONS.find((item) => item.value === type)?.label ?? |
| | | type ?? |
| | | '-' |
| | | ); |
| | | } |
| | | |
| | | const isEdit = computed(() => formType.value !== 'detail'); |
| | | const title = computed(() => { |
| | |
| | | schema: [], |
| | | showDefaultActions: false, |
| | | wrapperClass: 'grid-cols-2', |
| | | handleValuesChange: (values) => { |
| | | activityType.value = |
| | | (values.activityType as number) ?? ACTIVITY_TYPE.FORM; |
| | | }, |
| | | }); |
| | | |
| | | /** |
| | | * ææ´»å¨è¯¦æ
æ å°æè¡¨åå¼ |
| | | * æéä»¶å表转æä¸ä¼ ç»ä»¶è½è¯å«çæä»¶å¯¹è±¡ |
| | | * |
| | | * å°é¢å¨å端ä¸è½ä¸å¡è¡¨å段ï¼èæ¯åå¨éä»¶ä¸é´è¡¨éï¼ |
| | | * åæ¾æ¶è¦æ attachmentList 转æä¸ä¼ ç»ä»¶è½è¯å«çæä»¶å¯¹è±¡ã |
| | | * åæ¾å¯¹è±¡å¿
须带 responseï¼ç»ä»¶è®¾äº resultField ååªè®¤ item.responseï¼ |
| | | * ä¸å¸¦çè¯ãè¿ç¼è¾é¡µä¸å¨å¾ç´æ¥ä¿åãä¼èµ° URL 忝 â NaN â [] â å¾è¢«æ¸
空 |
| | | */ |
| | | function activityToForm( |
| | | activity: WxCpActivityApi.Activity, |
| | | ): Record<string, unknown> { |
| | | const form: Record<string, unknown> = { ...activity }; |
| | | form.blobIds = (activity.attachmentList ?? []).map((item) => { |
| | | function attachmentsToUploadFiles( |
| | | list: WxCpActivityApi.SignupAttachment[] | undefined, |
| | | ) { |
| | | return (list ?? []).map((item) => { |
| | | const name = item.name || item.originalFilename || ''; |
| | | const url = toAccessibleFileUrl(item.url || item.previewURL); |
| | | return { |
| | |
| | | url, |
| | | status: 'done', |
| | | id: item.id, |
| | | // åæ¾å¯¹è±¡ä¹å¿
须带 responseï¼ç»ä»¶è®¾äº resultField ååªè®¤ item.responseï¼ |
| | | // ä¸å¸¦çè¯ãè¿ç¼è¾é¡µä¸å¨å¾ç´æ¥ä¿åãä¼èµ° URL 忝 â NaN â [] â å°é¢è¢«æ¸
空 |
| | | response: { id: item.id, name, url }, |
| | | }; |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * ææ´»å¨è¯¦æ
æ å°æè¡¨åå¼ |
| | | * |
| | | * å°é¢ä¸è¯¦æ
å¾çå¨å端é½ä¸è½ä¸å¡è¡¨å段ï¼èæ¯åå¨éä»¶ä¸é´è¡¨éï¼ä¸¤å¥ recordTypeï¼ï¼ |
| | | * åæ¾æ¶å嫿 attachmentList / detailAttachmentList 转æä¸ä¼ ç»ä»¶çæä»¶å¯¹è±¡ã |
| | | */ |
| | | function activityToForm( |
| | | activity: WxCpActivityApi.Activity, |
| | | ): Record<string, unknown> { |
| | | const form: Record<string, unknown> = { ...activity }; |
| | | form.blobIds = attachmentsToUploadFiles(activity.attachmentList); |
| | | form.detailBlobIds = attachmentsToUploadFiles(activity.detailAttachmentList); |
| | | // å¾çéç¥ / éä»¶éç¥çç´ æèµ°åèªçéä»¶å
³ç³»ï¼åæ¾æ¶åæ ·è½¬æä¸ä¼ ç»ä»¶å¯¹è±¡ |
| | | form.imageBlobIds = attachmentsToUploadFiles(activity.imageAttachmentList); |
| | | form.fileBlobIds = attachmentsToUploadFiles(activity.fileAttachmentList); |
| | | return form; |
| | | } |
| | | |
| | |
| | | modalApi.lock(); |
| | | try { |
| | | const detail = await getActivity(data.id); |
| | | activityType.value = detail.activityType ?? ACTIVITY_TYPE.FORM; |
| | | fields.value = (detail.fieldList ?? []).map((item) => ({ ...item })); |
| | | await formApi.setValues(activityToForm(detail)); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | } else { |
| | | await formApi.resetForm(); |
| | | activityType.value = ACTIVITY_TYPE.FORM; |
| | | fields.value = []; |
| | | // é»è®¤ç»ãæ°æ®éé/è¯ä»·ãï¼ä¸æ¹é åçæ¥åæ´»å¨ä¸è´ |
| | | await formApi.setValues( |
| | | { |
| | | activityType: ACTIVITY_TYPE.FORM, |
| | | ratingEnabled: false, |
| | | ratingMax: 5, |
| | | signupLimit: 0, |
| | | }, |
| | | false, |
| | | ); |
| | | } |
| | | }, |
| | | }); |
| | |
| | | submitting.value = true; |
| | | try { |
| | | const data = (await formApi.getValues()) as Record<string, unknown>; |
| | | const type = data.activityType ?? ACTIVITY_TYPE.FORM; |
| | | const payload = { |
| | | ...data, |
| | | blobIds: normalizeBlobIds(data.blobIds), |
| | | detailBlobIds: normalizeBlobIds(data.detailBlobIds), |
| | | imageBlobIds: normalizeBlobIds(data.imageBlobIds), |
| | | fileBlobIds: normalizeBlobIds(data.fileBlobIds), |
| | | // åªææ°æ®é鿍¡æ¿æäº¤å段å®ä¹ï¼å
¶å®æ¨¡æ¿ä¸ä¼ ï¼é¿å
误æ¸
空 |
| | | fieldList: type === ACTIVITY_TYPE.FORM ? fields.value : undefined, |
| | | } as WxCpActivityApi.Activity; |
| | | if (id.value) { |
| | | await updateActivity({ ...payload, id: id.value }); |
| | |
| | | <Modal :title="title" class="w-3/5"> |
| | | <Form class="mx-4" /> |
| | | |
| | | <div v-if="showFieldEditor" class="mx-4 mt-2"> |
| | | <FieldEditor v-model="fields" /> |
| | | </div> |
| | | <div |
| | | v-else-if=" |
| | | formType === 'detail' && |
| | | activityType === ACTIVITY_TYPE.FORM && |
| | | fields.length |
| | | " |
| | | class="mx-4 mt-2" |
| | | > |
| | | <div class="mb-2 text-sm font-medium">ééåæ®µ</div> |
| | | <div |
| | | v-for="(item, index) in fields" |
| | | :key="index" |
| | | class="text-sm text-gray-600 dark:text-gray-400" |
| | | > |
| | | · {{ item.fieldLabel }}ï¼{{ fieldTypeLabel(item.fieldType) }}ï¼ |
| | | <span v-if="item.required" class="text-red-500">*</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="flex justify-end gap-2"> |
| | | <Button @click="modalApi.close()">åæ¶</Button> |
| | |
| | | <script lang="ts" setup> |
| | | import type { WxCpActivityApi } from '#/api/wxcp/activity'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { Alert, Button, message } from 'ant-design-vue'; |
| | | |
| | | import { pushActivity } from '#/api/wxcp/activity'; |
| | | import { ACTIVITY_TYPE, pushActivity } from '#/api/wxcp/activity'; |
| | | |
| | | import PushScopePicker from '../../components/push-scope-picker.vue'; |
| | | import { PUSH_SCOPE } from '../../data'; |
| | |
| | | const pushing = ref(false); |
| | | const resultMessage = ref(''); |
| | | const resultSuccess = ref(true); |
| | | |
| | | /** ææ¨¡æ¿è¯´ææ¬æ¬¡æ¨éä¼åä»ä¹ */ |
| | | const pushHint = computed(() => { |
| | | switch (activity.value.activityType) { |
| | | case ACTIVITY_TYPE.TEXT: |
| | | return 'æå伿¶å°ä¸æ¡çº¯ææ¬æ¶æ¯ï¼å
å®¹ä¸ºæ´»å¨æ é¢ + æ´»å¨è¯¦æ
ã'; |
| | | case ACTIVITY_TYPE.MESSAGE: |
| | | return 'æå伿¶å°ä¸å¼ å¯ç¹å»çå¡çæ¶æ¯ï¼ç¹å»æå¼æ´»å¨è¯¦æ
页ã'; |
| | | case ACTIVITY_TYPE.IMAGE: |
| | | return 'æåä¼å
æ¶å°æå说æï¼åéå¼ æ¶å°ä¸ä¼ çå¾çæ¶æ¯ã'; |
| | | case ACTIVITY_TYPE.FILE: |
| | | return 'æåä¼å
æ¶å°æå说æï¼åé个æ¶å°éä»¶ï¼å¯ç´æ¥ä¸è½½æ¥çã'; |
| | | default: |
| | | return 'æå伿¶å°å¡çæ¶æ¯ï¼ç¹å»æé®æå¼éé页填åå¹¶æäº¤è¯ä»·ã'; |
| | | } |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | onOpenChange(isOpen: boolean) { |
| | |
| | | <Modal title="æ¨éæ´»å¨" class="w-2/5"> |
| | | <div class="px-4"> |
| | | <div class="mb-4 rounded bg-gray-50 p-3 text-sm dark:bg-gray-800"> |
| | | <div class="mb-1 font-medium">{{ activity.title }}</div> |
| | | <div class="text-gray-500"> |
| | | æ¨éåæåçä¼ä¸å¾®ä¿¡ä¼æ¶å°å¡çæ¶æ¯ï¼ç¹å»ãç«å³æ¥åãç´æ¥æå¼æ¥å页ã |
| | | <div class="mb-1 font-medium"> |
| | | {{ activity.title }} |
| | | <span v-if="activity.activityTypeName" class="ml-2 text-xs text-gray-400"> |
| | | {{ activity.activityTypeName }} |
| | | </span> |
| | | </div> |
| | | <div class="text-gray-500">{{ pushHint }}</div> |
| | | </div> |
| | | |
| | | <div class="mb-2 text-sm font-medium">æ¨éèå´</div> |
| | |
| | | <span v-else class="text-gray-400">-</span> |
| | | </template> |
| | | |
| | | <template #fieldValues="{ row }"> |
| | | <div v-if="row.fieldValues?.length" class="space-y-1 text-xs"> |
| | | <div v-for="item in row.fieldValues" :key="item.fieldId"> |
| | | <span class="text-gray-500">{{ item.fieldLabel }}ï¼</span> |
| | | <span>{{ item.fieldValue }}</span> |
| | | </div> |
| | | </div> |
| | | <span v-else class="text-gray-400">-</span> |
| | | </template> |
| | | |
| | | <template #ratingScore="{ row }"> |
| | | <span v-if="row.ratingScore"> |
| | | {{ row.ratingScore }} æ |
| | | </span> |
| | | <span v-else class="text-gray-400">-</span> |
| | | </template> |
| | | |
| | | <template #attachment="{ row }"> |
| | | <Button type="link" class="p-0" @click="handleViewAttachments(row)"> |
| | | æ¥ç |
| | |
| | | /** 表åç±»å */ |
| | | export type NoticeFormType = 'create' | 'update' | 'detail'; |
| | | |
| | | /** éç¥æ¨¡æ¿ç±»å */ |
| | | export const NOTICE_TYPE = { |
| | | TEXT: 1, |
| | | CARD: 2, |
| | | FILE: 3, |
| | | NEWS: 4, |
| | | } as const; |
| | | |
| | | export const NOTICE_TYPE_OPTIONS = [ |
| | | { label: 'çº¯ææ¬éç¥', value: NOTICE_TYPE.TEXT }, |
| | | { label: '徿å¡çï¼å¡«æ¥/龿¥ï¼', value: NOTICE_TYPE.CARD }, |
| | | { label: 'æä»¶éç¥ï¼ä¸ä¼ æä»¶æ¨éï¼', value: NOTICE_TYPE.FILE }, |
| | | { label: 'å¾ææ¶æ¯ï¼æ é¢+æè¦+å°é¢ï¼', value: NOTICE_TYPE.NEWS }, |
| | | ]; |
| | | |
| | | /** éç¥è¡¨å */ |
| | | export function useNoticeFormSchema(formType: NoticeFormType): VbenFormSchema[] { |
| | | const readonly = formType === 'detail'; |
| | |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'noticeType', |
| | | label: 'éç¥æ¨¡æ¿', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: NOTICE_TYPE_OPTIONS, |
| | | placeholder: 'è¯·éæ©éç¥æ¨¡æ¿', |
| | | disabled: readonly, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'title', |
| | | label: 'éç¥æ é¢', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥éç¥æ é¢', disabled: readonly }, |
| | | rules: 'required', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'content', |
| | | label: 'éç¥æ£æ', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | rows: 8, |
| | | placeholder: '请è¾å
¥éç¥æ£æ', |
| | | rows: 6, |
| | | placeholder: 'çº¯ææ¬éç¥å¿
å¡«ï¼å
¶å®æ¨¡æ¿å¯ä½ä¸ºè¡¥å
说æ', |
| | | disabled: readonly, |
| | | }, |
| | | rules: 'required', |
| | | help: 'æ¨éæ¶è¶
è¿ 600 åä¼èªå¨æªæï¼å»ºè®®æå
³é®ä¿¡æ¯æ¾åé¢', |
| | | help: 'çº¯ææ¬éç¥æ¨éæ¶è¶
è¿ 600 åä¼èªå¨æªæï¼å»ºè®®æå
³é®ä¿¡æ¯æ¾åé¢', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'summary', |
| | | label: 'æè¦', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | rows: 3, |
| | | placeholder: '徿å¡ç/å¾ææ¶æ¯å±ç¤ºçæè¦', |
| | | disabled: readonly, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['noticeType'], |
| | | show: (values) => |
| | | values.noticeType === NOTICE_TYPE.CARD || |
| | | values.noticeType === NOTICE_TYPE.NEWS, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'linkUrl', |
| | | label: 'è·³è½¬é¾æ¥', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'å¡«æ¥é¡µé¢æè¯¦æ
页å°åï¼å¦ https://...', |
| | | disabled: readonly, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['noticeType'], |
| | | show: (values) => |
| | | values.noticeType === NOTICE_TYPE.CARD || |
| | | values.noticeType === NOTICE_TYPE.NEWS, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'linkBtnText', |
| | | label: 'æé®ææ¡', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'å¦ï¼å»å¡«æ¥ / æ¥ç详æ
', |
| | | disabled: readonly, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['noticeType'], |
| | | show: (values) => values.noticeType === NOTICE_TYPE.CARD, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'coverUrl', |
| | | label: 'å°é¢å¾å°å', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'å¾ææ¶æ¯å°é¢å¾å°åï¼éå¡«ï¼', |
| | | disabled: readonly, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['noticeType'], |
| | | show: (values) => values.noticeType === NOTICE_TYPE.NEWS, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'fileBlobId', |
| | | label: 'éç¥æä»¶', |
| | | component: 'FileUpload', |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | maxNumber: 1, |
| | | multiple: false, |
| | | showDescription: !readonly, |
| | | disabled: readonly, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['noticeType'], |
| | | show: (values) => values.noticeType === NOTICE_TYPE.FILE, |
| | | }, |
| | | help: 'ä¼ä¸å¾®ä¿¡æä»¶æ¶æ¯ä¸é 20MB', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | |
| | | export function useNoticeGridColumns(): VxeTableGridOptions<WxCpNoticeApi.Notice>['columns'] { |
| | | return [ |
| | | { |
| | | field: 'noticeTypeName', |
| | | title: '模æ¿', |
| | | width: 150, |
| | | }, |
| | | { |
| | | field: 'title', |
| | | title: 'éç¥æ é¢', |
| | | minWidth: 220, |
| | |
| | | updateNotice, |
| | | } from '#/api/wxcp/notice'; |
| | | |
| | | import { useNoticeFormSchema } from '../data'; |
| | | import { NOTICE_TYPE, useNoticeFormSchema } from '../data'; |
| | | |
| | | defineOptions({ name: 'WxCpNoticeForm' }); |
| | | |
| | |
| | | modalApi.lock(); |
| | | try { |
| | | const detail = await getNotice(data.id); |
| | | await formApi.setValues(detail as unknown as Record<string, unknown>); |
| | | const values: Record<string, unknown> = { ...detail }; |
| | | // æä»¶éç¥ï¼æ blobId è¿åæä¸ä¼ ç»ä»¶å¯è¯å«çæä»¶å¯¹è±¡ |
| | | if (detail.noticeType === NOTICE_TYPE.FILE && detail.fileBlobId) { |
| | | values.fileBlobId = [ |
| | | { |
| | | uid: String(detail.fileBlobId), |
| | | name: detail.fileName || '', |
| | | status: 'done', |
| | | id: detail.fileBlobId, |
| | | }, |
| | | ]; |
| | | } |
| | | await formApi.setValues(values); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | } else { |
| | | await formApi.resetForm(); |
| | | await formApi.setValues({ noticeType: NOTICE_TYPE.TEXT }, false); |
| | | } |
| | | }, |
| | | }); |
| | |
| | | submitting.value = true; |
| | | try { |
| | | const data = (await formApi.getValues()) as WxCpNoticeApi.Notice; |
| | | // ä¿åæ¶åªåæä»¶å½ä¸åï¼å
·ä½æ¨¡æ¿å¿
填项卿¨éæ¶ææéæ¨¡æ¿æ ¡éª |
| | | const noticeType = data.noticeType ?? NOTICE_TYPE.TEXT; |
| | | if (noticeType === NOTICE_TYPE.FILE) { |
| | | const raw = data.fileBlobId as unknown; |
| | | let blobId: number | undefined; |
| | | if (Array.isArray(raw)) { |
| | | const first = raw[0]; |
| | | blobId = |
| | | typeof first === 'number' |
| | | ? first |
| | | : (first as { id?: number } | undefined)?.id; |
| | | } else if (typeof raw === 'number') { |
| | | blobId = raw; |
| | | } else if (raw && typeof raw === 'object') { |
| | | blobId = (raw as { id?: number }).id; |
| | | } |
| | | data.fileBlobId = blobId; |
| | | } else { |
| | | data.fileBlobId = undefined; |
| | | } |
| | | if (id.value) { |
| | | await updateNotice({ ...data, id: id.value }); |
| | | message.success('ä¿®æ¹æå'); |
| | |
| | | <script lang="ts" setup> |
| | | import type { WxCpNoticeApi } from '#/api/wxcp/notice'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { Alert, Button, message } from 'ant-design-vue'; |
| | | import { Alert, Button, Input, Select, Textarea, message } from 'ant-design-vue'; |
| | | |
| | | import { pushNotice } from '#/api/wxcp/notice'; |
| | | import FileUpload from '#/components/upload/file-upload.vue'; |
| | | |
| | | import PushScopePicker from '../../components/push-scope-picker.vue'; |
| | | import { PUSH_SCOPE } from '../../data'; |
| | | import { NOTICE_TYPE, NOTICE_TYPE_OPTIONS } from '../data'; |
| | | |
| | | defineOptions({ name: 'WxCpNoticePushModal' }); |
| | | |
| | |
| | | const pushing = ref(false); |
| | | const resultMessage = ref(''); |
| | | |
| | | // æ¨éæ¶éæ©çæ¶æ¯æ¨¡æ¿ä¸åæ°ï¼é»è®¤åéç¥ä¸å·²ä¿åçé
ç½®ï¼ |
| | | const noticeType = ref<number>(NOTICE_TYPE.TEXT); |
| | | const summary = ref(''); |
| | | const linkUrl = ref(''); |
| | | const linkBtnText = ref(''); |
| | | const coverUrl = ref(''); |
| | | const fileName = ref(''); |
| | | const fileList = ref<number[]>([]); |
| | | |
| | | const isCard = computed(() => noticeType.value === NOTICE_TYPE.CARD); |
| | | const isNews = computed(() => noticeType.value === NOTICE_TYPE.NEWS); |
| | | const isFile = computed(() => noticeType.value === NOTICE_TYPE.FILE); |
| | | const showLink = computed(() => isCard.value || isNews.value); |
| | | |
| | | const pushHint = computed(() => { |
| | | switch (noticeType.value) { |
| | | case NOTICE_TYPE.CARD: |
| | | return 'æå伿¶å°å¾æå¡çï¼ç¹å»æé®å¯è·³è½¬å°å¡«æ¥æè¯¦æ
页é¢ã'; |
| | | case NOTICE_TYPE.FILE: |
| | | return 'æå伿¶å°ä¸æ¡è¯´æææ¬åä¸ä¸ªæä»¶ï¼æä»¶ä¸é 20MBã'; |
| | | case NOTICE_TYPE.NEWS: |
| | | return 'æå伿¶å°å¾ææ¶æ¯ï¼å±ç¤ºæ é¢ãæè¦ãå°é¢å¹¶å¯ç¹å»è·³è½¬ã'; |
| | | default: |
| | | return 'æå伿¶å°çº¯ææ¬æ¶æ¯ï¼å
容è¶
è¿ 600 åä¼èªå¨æªæã'; |
| | | } |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ notice: WxCpNoticeApi.Notice }>(); |
| | | notice.value = data.notice ?? {}; |
| | | scope.value = data.notice?.pushScope ?? PUSH_SCOPE.ALL; |
| | | target.value = data.notice?.pushTarget ?? ''; |
| | | const item = data.notice ?? {}; |
| | | notice.value = item; |
| | | scope.value = item.pushScope ?? PUSH_SCOPE.ALL; |
| | | target.value = item.pushTarget ?? ''; |
| | | noticeType.value = item.noticeType ?? NOTICE_TYPE.TEXT; |
| | | summary.value = item.summary ?? ''; |
| | | linkUrl.value = item.linkUrl ?? ''; |
| | | linkBtnText.value = item.linkBtnText ?? ''; |
| | | coverUrl.value = item.coverUrl ?? ''; |
| | | fileName.value = item.fileName ?? ''; |
| | | fileList.value = item.fileBlobId ? [item.fileBlobId] : []; |
| | | }, |
| | | }); |
| | | |
| | | /** ä¸ä¼ ç»ä»¶å¯è½è¿åæ°ç»æå¯¹è±¡ï¼ç»ä¸ååº blobId */ |
| | | function resolveFileBlobId(): number | undefined { |
| | | const raw = fileList.value as unknown; |
| | | if (Array.isArray(raw)) { |
| | | const first = raw[0]; |
| | | if (typeof first === 'number') { |
| | | return first; |
| | | } |
| | | return (first as { id?: number } | undefined)?.id; |
| | | } |
| | | return typeof raw === 'number' ? raw : undefined; |
| | | } |
| | | |
| | | async function handlePush() { |
| | | if (!notice.value.id) { |
| | |
| | | message.warning('éæ©ãæå®é¨é¨ãæãæå®æåãæ¶ï¼è¯·å
éæ©æ¨éç®æ '); |
| | | return; |
| | | } |
| | | if (noticeType.value === NOTICE_TYPE.TEXT && !notice.value.content?.trim()) { |
| | | message.warning('çº¯ææ¬éç¥çæ£æä¸è½ä¸ºç©º'); |
| | | return; |
| | | } |
| | | if (showLink.value && !linkUrl.value.trim()) { |
| | | message.warning('徿å¡ç/å¾ææ¶æ¯çè·³è½¬é¾æ¥ä¸è½ä¸ºç©º'); |
| | | return; |
| | | } |
| | | const fileBlobId = resolveFileBlobId(); |
| | | if (isFile.value && !fileBlobId) { |
| | | message.warning('æä»¶éç¥å¿
é¡»ä¸ä¼ æä»¶'); |
| | | return; |
| | | } |
| | | |
| | | pushing.value = true; |
| | | resultMessage.value = ''; |
| | | try { |
| | |
| | | id: notice.value.id, |
| | | pushScope: scope.value, |
| | | pushTarget: scope.value === PUSH_SCOPE.ALL ? '' : target.value, |
| | | noticeType: noticeType.value, |
| | | summary: summary.value, |
| | | linkUrl: linkUrl.value, |
| | | linkBtnText: linkBtnText.value, |
| | | coverUrl: coverUrl.value, |
| | | fileBlobId, |
| | | fileName: fileName.value || undefined, |
| | | }); |
| | | if (res?.success) { |
| | | message.success('æ¨éæå'); |
| | |
| | | <div class="px-4"> |
| | | <div class="mb-4 rounded bg-gray-50 p-3 text-sm dark:bg-gray-800"> |
| | | <div class="mb-1 font-medium">{{ notice.title }}</div> |
| | | <div class="text-gray-500"> |
| | | æ¨éåæåçä¼ä¸å¾®ä¿¡ä¼æ¶å°çº¯ææ¬æ¶æ¯ï¼å
容è¶
è¿ 600 åä¼èªå¨æªæã |
| | | </div> |
| | | <div class="text-gray-500">{{ pushHint }}</div> |
| | | </div> |
| | | |
| | | <div class="mb-2 text-sm font-medium">æ¨éèå´</div> |
| | | <div class="mb-2 text-sm font-medium">æ¶æ¯æ¨¡æ¿</div> |
| | | <Select |
| | | v-model:value="noticeType" |
| | | :options="NOTICE_TYPE_OPTIONS" |
| | | class="mb-4 w-full" |
| | | placeholder="è¯·éæ©æ¨é模æ¿ç±»å" |
| | | /> |
| | | |
| | | <template v-if="showLink"> |
| | | <div class="mb-2 text-sm font-medium">æè¦</div> |
| | | <Textarea |
| | | v-model:value="summary" |
| | | class="mb-3" |
| | | :rows="2" |
| | | placeholder="徿å¡ç/å¾ææ¶æ¯å±ç¤ºçæè¦" |
| | | /> |
| | | <div class="mb-2 text-sm font-medium">è·³è½¬é¾æ¥</div> |
| | | <Input |
| | | v-model:value="linkUrl" |
| | | class="mb-3" |
| | | placeholder="å¡«æ¥é¡µé¢æè¯¦æ
页å°åï¼å¦ https://..." |
| | | /> |
| | | </template> |
| | | |
| | | <template v-if="isCard"> |
| | | <div class="mb-2 text-sm font-medium">æé®ææ¡</div> |
| | | <Input |
| | | v-model:value="linkBtnText" |
| | | class="mb-3" |
| | | placeholder="å¦ï¼å»å¡«æ¥ / æ¥ç详æ
" |
| | | /> |
| | | </template> |
| | | |
| | | <template v-if="isNews"> |
| | | <div class="mb-2 text-sm font-medium">å°é¢å¾å°å</div> |
| | | <Input |
| | | v-model:value="coverUrl" |
| | | class="mb-3" |
| | | placeholder="å°é¢å¾å°åï¼éå¡«ï¼" |
| | | /> |
| | | </template> |
| | | |
| | | <template v-if="isFile"> |
| | | <div class="mb-2 text-sm font-medium">éç¥æä»¶</div> |
| | | <FileUpload |
| | | v-model:value="fileList" |
| | | class="mb-3" |
| | | :max-number="1" |
| | | :multiple="false" |
| | | value-key="id" |
| | | :show-description="true" |
| | | /> |
| | | </template> |
| | | |
| | | <div class="mb-2 mt-4 text-sm font-medium">æ¨éèå´</div> |
| | | <PushScopePicker v-model:scope="scope" v-model:target="target" /> |
| | | |
| | | <Alert |