feat(app): 移除 App 端全部附件上传、下载、预览功能
按要求 App 不再提供任何附件能力:
- 删除 4 处跟进图片上传(客户/合同/线索/商机)与素材 SIP/SOP 图片上传
- 删除售后工单/问题单附件展示、退货质检报告预览
- 删除采购/销售订单附件列表、供应商证书附件、发票与报价附件状态
- 删除设计资料预览、归档资料预览下载、BPM 业务附件配置页及其路由
- 清理各 api 文件中已无调用方的 upload*/choose*File 上传封装
Co-Authored-By: Claude Code <noreply@anthropic.com>
| | |
| | | export function getVisit(token: string, id: number) { return request<AftersalesVisit>(`/aftersales/visit/get${query({ id })}`, { header: header(token) }); } |
| | | export function getVisitListByTicketId(token: string, ticketId: number) { return request<AftersalesVisit[]>(`/aftersales/visit/list${query({ ticketId })}`, { header: header(token) }); } |
| | | export function getVisitPage(token: string, params: { pageNo: number; pageSize: number; ticketId?: number }) { return request<PageResult<AftersalesVisit>>(`/aftersales/visit/page${query(params)}`, { header: header(token) }); } |
| | | |
| | | export function uploadAftersalesFile(token: string, filePath: string) { |
| | | return new Promise<AftersalesAttachment>((resolve, reject) => { |
| | | uni.uploadFile({ url: `${API_BASE_URL}/system/storage-blob/upload`, filePath, name: "files", header: header(token), |
| | | formData: { directory: "aftersales" }, success: response => { |
| | | try { |
| | | const result = JSON.parse(response.data) as ApiResponse<AftersalesAttachment[]>; |
| | | if (result.code === 0 && result.data?.[0]) resolve(result.data[0]); |
| | | else reject(new Error(result.msg || "æä»¶ä¸ä¼ 失败")); |
| | | } catch { reject(new Error("æä»¶ä¸ä¼ ååºè§£æå¤±è´¥")); } |
| | | }, fail: reject }); |
| | | }); |
| | | } |
| | | export function chooseAftersalesFiles(count = 10): Promise<Array<{ path: string; name?: string }>> { |
| | | return new Promise((resolve, reject) => { |
| | | const done = (files: Array<{ path: string; name?: string }>) => resolve(files.filter(item => Boolean(item.path))); |
| | | // #ifdef MP-WEIXIN |
| | | uni.chooseMessageFile({ count, type: "file", success: result => done((result.tempFiles || []).map(item => ({ path: item.path, name: item.name }))), fail: reject }); |
| | | // #endif |
| | | // #ifndef MP-WEIXIN |
| | | uni.chooseFile({ count, success: result => done((result.tempFiles || []).map(item => ({ path: item.path, name: item.name }))), fail: reject }); |
| | | // #endif |
| | | }); |
| | | } |
| | |
| | | return request<boolean>(`/bpm/process-expression/delete${query({ id })}`, { method: "DELETE", header: header(accessToken) }); |
| | | } |
| | | |
| | | export interface BpmBusinessAttach { |
| | | id?: number; |
| | | name: string; |
| | | code?: string; |
| | | status?: number; |
| | | processDefinitionKey?: string; |
| | | taskDefinitionKey?: string; |
| | | fileTypes?: string; |
| | | maxSize?: number; |
| | | required?: boolean; |
| | | remark?: string; |
| | | createTime?: number | string; |
| | | } |
| | | |
| | | export function getBusinessAttachPage(accessToken: string, params: { pageNo: number; pageSize: number; name?: string }) { |
| | | return request<PageResult<BpmBusinessAttach>>(`/bpm/business-attach/page${query(params)}`, { header: header(accessToken) }); |
| | | } |
| | | |
| | | export function getBusinessAttach(accessToken: string, id: number) { |
| | | return request<BpmBusinessAttach>(`/bpm/business-attach/get${query({ id })}`, { header: header(accessToken) }); |
| | | } |
| | | |
| | | export function createBusinessAttach(accessToken: string, data: BpmBusinessAttach) { |
| | | return request<number>("/bpm/business-attach/create", { method: "POST", data, header: header(accessToken) }); |
| | | } |
| | | |
| | | export function updateBusinessAttach(accessToken: string, data: BpmBusinessAttach) { |
| | | return request<boolean>("/bpm/business-attach/update", { method: "PUT", data, header: header(accessToken) }); |
| | | } |
| | | |
| | | export function deleteBusinessAttach(accessToken: string, id: number) { |
| | | return request<boolean>(`/bpm/business-attach/delete${query({ id })}`, { method: "DELETE", header: header(accessToken) }); |
| | | } |
| | | |
| | | /* ---------- æµç¨å®ä¹ ---------- */ |
| | | |
| | | /** å¯ç¨ä¸çæµç¨å®ä¹å表ï¼åèµ·æµç¨é¡µé¢ç¨ï¼suspensionState 为å¿
å¡«ï¼ */ |
| | |
| | | `/bpm/task/list-by-process-instance-id?processInstanceId=${encodeURIComponent(processInstanceId)}`, |
| | | { header: header(accessToken) }, |
| | | ); |
| | | } |
| | | |
| | | /* ---------- æä»¶ä¸ä¼ ---------- */ |
| | | |
| | | export interface BpmUploadResult { |
| | | id?: number; |
| | | name?: string; |
| | | url?: string; |
| | | path?: string; |
| | | size?: number; |
| | | type?: string; |
| | | } |
| | | |
| | | /** ä¸ä¼ BPM éä»¶ / ç¾åå¾çï¼å段å fileï¼ä¸ web 端 requestClient.upload ä¸è´ï¼ */ |
| | | export function uploadBpmFile(accessToken: string, filePath: string, directory = "bpm/task-attachment") { |
| | | return new Promise<BpmUploadResult>((resolve, reject) => { |
| | | uni.uploadFile({ |
| | | url: `${API_BASE_URL}/infra/file/upload`, |
| | | filePath, |
| | | name: "file", |
| | | header: { Authorization: `Bearer ${accessToken}` }, |
| | | formData: { directory }, |
| | | success: response => { |
| | | try { |
| | | const data = JSON.parse(response.data) as { |
| | | code?: number; |
| | | msg?: string; |
| | | data?: BpmUploadResult; |
| | | }; |
| | | if (data.code === 0 && data.data) resolve(data.data); |
| | | else reject(new Error(data.msg || "æä»¶ä¸ä¼ 失败")); |
| | | } catch { |
| | | reject(new Error("æä»¶ä¸ä¼ ååºè§£æå¤±è´¥")); |
| | | } |
| | | }, |
| | | fail: () => reject(new Error("æä»¶ä¸ä¼ 失败ï¼è¯·æ£æ¥ç½ç»åéè¯")), |
| | | }); |
| | | }); |
| | | } |
| | | |
| | | /** æ¬å°éä¸çå¾
ä¸ä¼ æä»¶ */ |
| | | export interface BpmPickedFile { |
| | | path: string; |
| | | name?: string; |
| | | } |
| | | |
| | | /** |
| | | * 跨端æä»¶éæ©ï¼H5 / 微信å°ç¨åº / App ä¸ç«¯å
¼å®¹ï¼ |
| | | * - H5 / Appï¼uni.chooseFile |
| | | * - 微信å°ç¨åºï¼uni.chooseMessageFileï¼uni.chooseFile å¨å°ç¨åºä¸å¯ç¨ï¼ |
| | | */ |
| | | export function chooseBpmFile(count = 9): Promise<BpmPickedFile[]> { |
| | | return new Promise((resolve, reject) => { |
| | | const onSuccess = (files: BpmPickedFile[]) => resolve(files); |
| | | const onFail = (error: unknown) => reject(error); |
| | | |
| | | // #ifdef MP-WEIXIN |
| | | uni.chooseMessageFile({ |
| | | count, |
| | | type: "file", |
| | | success: result => { |
| | | onSuccess( |
| | | (result.tempFiles || []) |
| | | .map(item => ({ path: item.path, name: item.name })) |
| | | .filter(item => Boolean(item.path)), |
| | | ); |
| | | }, |
| | | fail: onFail, |
| | | }); |
| | | // #endif |
| | | |
| | | // #ifndef MP-WEIXIN |
| | | uni.chooseFile({ |
| | | count, |
| | | success: result => { |
| | | onSuccess( |
| | | (result.tempFiles || []) |
| | | .map(item => ({ path: item.path, name: item.name })) |
| | | .filter(item => Boolean(item.path)), |
| | | ); |
| | | }, |
| | | fail: onFail, |
| | | }); |
| | | // #endif |
| | | }); |
| | | } |
| | | |
| | | /** è·¨ç«¯éæ©å¾çï¼ç¾åå¾çãå¾çç±»éä»¶ï¼ */ |
| | | export function chooseBpmImage(count = 1): Promise<BpmPickedFile[]> { |
| | | return new Promise((resolve, reject) => { |
| | | uni.chooseImage({ |
| | | count, |
| | | success: result => { |
| | | resolve((result.tempFilePaths || []).map(path => ({ path }))); |
| | | }, |
| | | fail: reject, |
| | | }); |
| | | }); |
| | | } |
| | |
| | | }); |
| | | }); |
| | | } |
| | | export function uploadCrmFile(accessToken: string, filePath: string) { |
| | | return new Promise<StorageFile>((resolve, reject) => |
| | | uni.uploadFile({ |
| | | url: `${API_BASE_URL}/system/storage-blob/upload`, |
| | | filePath, |
| | | name: 'files', |
| | | header: header(accessToken), |
| | | success: response => { |
| | | try { |
| | | const data = JSON.parse(response.data) as ApiResponse<StorageFile[]>; |
| | | if (data.code === 0 && data.data?.[0]) { |
| | | resolve(data.data[0]); |
| | | } else { |
| | | reject(new Error(data.msg || 'æä»¶ä¸ä¼ 失败')); |
| | | } |
| | | } catch { |
| | | reject(new Error('æä»¶ä¸ä¼ ååºè§£æå¤±è´¥')); |
| | | } |
| | | }, |
| | | fail: () => reject(new Error('æä»¶ä¸ä¼ 失败ï¼è¯·æ£æ¥ç½ç»åéè¯')), |
| | | }), |
| | | ); |
| | | } |
| | |
| | | export function clockIn(token: string, data: AttendanceClockRequest) { return request<AttendanceClockResponse>("/hrm/attendance/record/clock-in", { method: "POST", data, header: header(token) }); } |
| | | export function clockOut(token: string, data: AttendanceClockRequest) { return request<AttendanceClockResponse>("/hrm/attendance/record/clock-out", { method: "POST", data, header: header(token) }); } |
| | | |
| | | export function uploadHrmFile(token: string, filePath: string) { |
| | | return new Promise<HrmAttachment>((resolve, reject) => { |
| | | uni.uploadFile({ |
| | | url: `${API_BASE_URL}/system/storage-blob/upload`, |
| | | filePath, |
| | | name: "files", |
| | | header: header(token), |
| | | formData: { directory: "hrm" }, |
| | | success: response => { |
| | | try { |
| | | const result = JSON.parse(response.data) as ApiResponse<HrmAttachment[]>; |
| | | if (result.code === 0 && result.data?.[0]) resolve(result.data[0]); |
| | | else reject(new Error(result.msg || "æä»¶ä¸ä¼ 失败")); |
| | | } catch { |
| | | reject(new Error("æä»¶ä¸ä¼ ååºè§£æå¤±è´¥")); |
| | | } |
| | | }, |
| | | fail: () => reject(new Error("æä»¶ä¸ä¼ 失败ï¼è¯·æ£æ¥ç½ç»åéè¯")) |
| | | }); |
| | | }); |
| | | } |
| | | |
| | | export function chooseHrmFiles(count = 10): Promise<Array<{ path: string; name?: string }>> { |
| | | return new Promise((resolve, reject) => { |
| | | const complete = (files: Array<{ path: string; name?: string }>) => resolve(files.filter(item => Boolean(item.path))); |
| | | // #ifdef MP-WEIXIN |
| | | uni.chooseMessageFile({ count, type: "file", success: result => complete((result.tempFiles || []).map(item => ({ path: item.path, name: item.name }))), fail: reject }); |
| | | // #endif |
| | | // #ifndef MP-WEIXIN |
| | | uni.chooseFile({ count, success: result => complete((result.tempFiles || []).map(item => ({ path: item.path, name: item.name }))), fail: reject }); |
| | | // #endif |
| | | }); |
| | | } |
| | | |
| | | export function getEmployeePage(token: string, params: { pageNo: number; pageSize: number; name?: string; employeeNo?: string; employeeStatus?: number }) { return request<PageResult<Employee>>(`/hrm/employee/page${query(params)}`, { header: header(token) }); } |
| | | export function getEmployee(token: string, id: number) { return request<Employee>(`/hrm/employee/get${query({ id })}`, { header: header(token) }); } |
| | | export function createEmployee(token: string, data: Employee) { return request<number>("/hrm/employee/create", { method: "POST", data, header: header(token) }); } |
| | |
| | | export function saveBatchConfig(accessToken: string, data: MesBatchConfig) { |
| | | return request<number>("/mes/md/item-batch-config/save", { method: "POST", data, header: header(accessToken) }); |
| | | } |
| | | export function uploadMesFile(accessToken: string, filePath: string) { |
| | | return new Promise<StorageFile>((resolve, reject) => |
| | | uni.uploadFile({ |
| | | url: `${API_BASE_URL}/system/storage-blob/upload`, |
| | | filePath, |
| | | name: "files", |
| | | header: header(accessToken), |
| | | success: response => { |
| | | try { |
| | | const data = JSON.parse(response.data) as ApiResponse<StorageFile[]>; |
| | | if ((data.code === 0 || data.code === 200) && data.data?.[0]) resolve(data.data[0]); |
| | | else reject(new Error(data.msg || "æä»¶ä¸ä¼ 失败")); |
| | | } catch { |
| | | reject(new Error("æä»¶ä¸ä¼ ååºè§£æå¤±è´¥")); |
| | | } |
| | | }, |
| | | fail: () => reject(new Error("æä»¶ä¸ä¼ 失败ï¼è¯·æ£æ¥ç½ç»åéè¯")), |
| | | }), |
| | | ); |
| | | } |
| | | export function getProductSipListByItemId(accessToken: string, itemId: number) { |
| | | return request<ProductMedia[]>(`/mes/md/product-sip/list-by-item-id${query({ itemId })}`, { header: header(accessToken) }); |
| | | } |
| | |
| | | type?: string; |
| | | createTime?: number; |
| | | } |
| | | |
| | | /** |
| | | * æä»¶ä¸ä¼ ï¼è®¾è®¡èµæ blobIdsï¼ |
| | | * ä¸ web 端 FileUpload ç»ä»¶èµ°åä¸å端æ¥å£ /system/storage-blob/upload |
| | | */ |
| | | export function uploadPdFile(accessToken: string, filePath: string) { |
| | | return new Promise<StorageAttachment>((resolve, reject) => { |
| | | uni.uploadFile({ |
| | | url: `${API_BASE_URL}/system/storage-blob/upload`, |
| | | filePath, |
| | | name: "files", |
| | | header: { Authorization: `Bearer ${accessToken}` }, |
| | | formData: { directory: "mes/pd" }, |
| | | success: response => { |
| | | try { |
| | | const data = JSON.parse(response.data) as ApiResponse<StorageAttachment[]>; |
| | | if (data.code === 0 && data.data?.[0]) resolve(data.data[0]); |
| | | else reject(new Error(data.msg || "æä»¶ä¸ä¼ 失败")); |
| | | } catch { |
| | | reject(new Error("æä»¶ä¸ä¼ ååºè§£æå¤±è´¥")); |
| | | } |
| | | }, |
| | | fail: () => reject(new Error("æä»¶ä¸ä¼ 失败ï¼è¯·æ£æ¥ç½ç»åéè¯")), |
| | | }); |
| | | }); |
| | | } |
| | | |
| | | /** æ¬å°éä¸çå¾
ä¸ä¼ æä»¶ */ |
| | | export interface PickedFile { |
| | | path: string; |
| | | name?: string; |
| | | } |
| | | |
| | | /** |
| | | * 跨端æä»¶éæ©ï¼H5 / 微信å°ç¨åº / App ä¸ç«¯å
¼å®¹ï¼ |
| | | * |
| | | * å¹³å°å·®å¼ï¼uni-app 宿¹æ¯æç©éµï¼ï¼ |
| | | * - H5 ï¼uni.chooseFile å¯ç¨ï¼æµè§å¨æ¯ <input type="file">ï¼ |
| | | * - 微信å°ç¨åºï¼uni.chooseFile **ä¸å¯ç¨**ï¼éç¨ uni.chooseMessageFile ä»è天记å½éæä»¶ |
| | | * - App ï¼uni.chooseFile å¯ç¨ |
| | | * |
| | | * å æ¤è¿éç¨æ¡ä»¶ç¼è¯åæµï¼ç»ä¸è¿å { path, name }[]ï¼è°ç¨æ¹æ éå
³å¿å¹³å°ã |
| | | */ |
| | | export function choosePdFile(count = 9): Promise<PickedFile[]> { |
| | | return new Promise((resolve, reject) => { |
| | | const onSuccess = (files: PickedFile[]) => resolve(files); |
| | | const onFail = (error: unknown) => reject(error); |
| | | |
| | | // #ifdef MP-WEIXIN |
| | | uni.chooseMessageFile({ |
| | | count, |
| | | type: "file", |
| | | success: result => { |
| | | onSuccess( |
| | | (result.tempFiles || []) |
| | | .map(item => ({ path: item.path, name: item.name })) |
| | | .filter(item => Boolean(item.path)), |
| | | ); |
| | | }, |
| | | fail: onFail, |
| | | }); |
| | | // #endif |
| | | |
| | | // #ifndef MP-WEIXIN |
| | | uni.chooseFile({ |
| | | count, |
| | | success: result => { |
| | | onSuccess( |
| | | (result.tempFiles || []) |
| | | .map(item => ({ path: item.path, name: item.name })) |
| | | .filter(item => Boolean(item.path)), |
| | | ); |
| | | }, |
| | | fail: onFail, |
| | | }); |
| | | // #endif |
| | | }); |
| | | } |
| | |
| | | export const createTenderProject = (token: string, data: TenderProject) => request<number>('/srm/tender-project/create', options(token, 'POST', data)); |
| | | export const updateTenderProject = (token: string, data: TenderProject) => request<boolean>('/srm/tender-project/update', options(token, 'PUT', data)); |
| | | export const updateTenderProjectInfo = (token: string, data: Pick<TenderProject, 'id' | 'tenderDesc' | 'qualificationRequirements' | 'blobIds'>) => request<boolean>('/srm/tender-project/update-info', options(token, 'PUT', data)); |
| | | export function uploadTenderFile(accessToken: string, filePath: string) { |
| | | return new Promise<StorageAttachment>((resolve, reject) => { |
| | | uni.uploadFile({ |
| | | url: `${API_BASE_URL}/system/storage-blob/upload`, |
| | | filePath, |
| | | name: 'files', |
| | | header: authHeader(accessToken), |
| | | success: response => { |
| | | try { |
| | | const data = JSON.parse(response.data) as ApiResponse<StorageAttachment[]>; |
| | | if (data.code === 0 && data.data?.[0]) resolve(data.data[0]); |
| | | else reject(new Error(data.msg || 'æä»¶ä¸ä¼ 失败')); |
| | | } catch { reject(new Error('æä»¶ä¸ä¼ ååºè§£æå¤±è´¥')); } |
| | | }, |
| | | fail: () => reject(new Error('æä»¶ä¸ä¼ 失败ï¼è¯·æ£æ¥ç½ç»åéè¯')), |
| | | }); |
| | | }); |
| | | } |
| | | export const deleteTenderProject = (token: string, id: number) => request<boolean>(`/srm/tender-project/delete${toQuery({ id })}`, options(token, 'DELETE')); |
| | | export const publishTender = (token: string, id: number) => request<boolean>(`/srm/tender-project/publish${toQuery({ id })}`, options(token, 'PUT')); |
| | | export const closeTender = (token: string, id: number) => request<boolean>(`/srm/tender-project/close${toQuery({ id })}`, options(token, 'PUT')); |
| | |
| | | export function reviewQuality(accessToken: string, data: ApplyReview) { return request<boolean>('/srm/supplier-apply/review-quality', { method: 'PUT', data, header: authHeader(accessToken) }); } |
| | | export function reviewFinance(accessToken: string, data: ApplyReview) { return request<boolean>('/srm/supplier-apply/review-finance', { method: 'PUT', data, header: authHeader(accessToken) }); } |
| | | |
| | | export function uploadCertificateFile(accessToken: string, filePath: string) { |
| | | return new Promise<StorageAttachment>((resolve, reject) => { |
| | | uni.uploadFile({ |
| | | url: `${API_BASE_URL}/system/storage-blob/upload`, |
| | | filePath, |
| | | name: 'files', |
| | | header: authHeader(accessToken), |
| | | success: response => { |
| | | try { |
| | | const data = JSON.parse(response.data) as ApiResponse<StorageAttachment[]>; |
| | | if (data.code === 0 && data.data?.[0]) resolve(data.data[0]); |
| | | else reject(new Error(data.msg || 'æä»¶ä¸ä¼ 失败')); |
| | | } catch { reject(new Error('æä»¶ä¸ä¼ ååºè§£æå¤±è´¥')); } |
| | | }, |
| | | fail: () => reject(new Error('æä»¶ä¸ä¼ 失败ï¼è¯·æ£æ¥ç½ç»åéè¯')), |
| | | }); |
| | | }); |
| | | } |
| | | |
| | | export function getCertificatePage(accessToken: string, pageNo: number, supplierName?: string) { return request<PageResult<SupplierCertificateItem>>(`/srm/supplier-certificate/page${pageParams(pageNo, 15, { supplierName })}`, { header: authHeader(accessToken) }); } |
| | | export function getCertificate(accessToken: string, id: number) { return request<SupplierCertificateItem>(`/srm/supplier-certificate/get${toQuery({ id })}`, { header: authHeader(accessToken) }); } |
| | | export function createCertificate(accessToken: string, data: SupplierCertificateItem) { return request<number>('/srm/supplier-certificate/create', { method: 'POST', data, header: authHeader(accessToken) }); } |
| | |
| | | "bpm/group/index": "/pages/bpm/group/index", |
| | | "bpm/processListener/index": "/pages/bpm/process-listener/index", |
| | | "bpm/processExpression/index": "/pages/bpm/process-expression/index", |
| | | "bpm/businessAttach/index": "/pages/bpm/business-attach/index", |
| | | "bpm/processInstance/manager/index": "/pages/bpm/process-instance/manager/index", |
| | | "bpm/task/manager/index": "/pages/bpm/task/manager/index", |
| | | "ai/model/apiKey/index": "/pages/ai/api-key/index", |
| | |
| | | } |
| | | }, |
| | | { |
| | | "path": "business-attach/index", |
| | | "style": { |
| | | "navigationBarTitleText": "ä¸å¡éä»¶é
ç½®" |
| | | } |
| | | }, |
| | | { |
| | | "path": "business-attach/form", |
| | | "style": { |
| | | "navigationBarTitleText": "ä¸å¡éä»¶é
ç½®ç¼è¾" |
| | | } |
| | | }, |
| | | { |
| | | "path": "process-instance/manager/index", |
| | | "style": { |
| | | "navigationBarTitleText": "æµç¨å®ä¾" |
| | |
| | | import { createProblem, getProblem, updateProblem, type AftersalesProblem } from "../../../api/aftersales"; |
| | | import { useAuth } from "../../../stores/auth"; |
| | | import PickerSheet from "../../mes/process-design/components/PickerSheet.vue"; |
| | | import AftersalesAttachmentField from "../AftersalesAttachmentField.vue"; |
| | | import { ISSUE_TYPES, PROBLEM_STATUS, ROOT_CAUSES, SEVERITIES, labelOf, toast } from "../shared"; |
| | | |
| | | const { hasPermission, state } = useAuth(); |
| | |
| | | <view class="field"><text>æ ¹å åç±»</text><view class="picker" @click="!readonly && (rootVisible = true)"><text>{{ ROOT_CAUSES[form.rootCauseCategory || 0] || "è¯·éæ©" }}</text><text>âº</text></view></view> |
| | | <view class="field"><text>ç©æç¼ç </text><input v-model="form.itemCode" :disabled="readonly" placeholder="æ¥æºäºå·¥åæç»" /></view><view class="field"><text>ç©æåç§°</text><input v-model="form.itemName" :disabled="readonly" placeholder="æ¥æºäºå·¥åæç»" /></view><view class="field"><text>æ¹æ¬¡å·</text><input v-model="form.batchCode" :disabled="readonly" placeholder="æ¥æºäºå·¥åæç»" /></view> |
| | | <view class="field column"><text>é®é¢æè¿°</text><textarea v-model="form.description" :disabled="readonly" placeholder="é®é¢ç°è±¡ä¸èæ¯" /></view><view class="field column"><text>æ ¹å åæ</text><textarea v-model="form.rootCause" :disabled="readonly" placeholder="缺é·äº§ççæ ¹æ¬åå " /></view><view class="field column"><text>è§£å³æ¹æ¡</text><textarea v-model="form.solution" :disabled="readonly" placeholder="å¤çæªæ½/SOPï¼ä¾åç»å·¥ååè" /></view><view class="field column"><text>夿³¨</text><textarea v-model="form.remark" :disabled="readonly" placeholder="夿³¨" /></view> |
| | | <AftersalesAttachmentField v-model="form.blobIds" v-model:files="form.attachmentList" :token="state.accessToken" :readonly="readonly" /> |
| | | </view> |
| | | <button v-if="canSave" class="save" :loading="submitting" @click="submit">ä¿å</button> |
| | | <PickerSheet v-model:show="issueVisible" title="é®é¢ç±»å" :items="ISSUE_TYPES" :selected-keys="form.issueType ? [form.issueType] : []" :searchable="false" @confirm="keys => form.issueType = Number(keys[0])" /> |
| | |
| | | |
| | | async function load() { if (state.accessToken) data.value = await getReturn(state.accessToken, id.value); } |
| | | function goTicket() { if (data.value?.ticketId) uni.navigateTo({ url: `/pages/aftersales/ticket/detail?id=${data.value.ticketId}` }); } |
| | | function previewReport() { |
| | | if (!data.value?.qualityReportUrl) return; |
| | | const url = data.value.qualityReportUrl; |
| | | if (/\.(png|jpe?g|gif|webp|bmp)(\?.*)?$/i.test(url)) return uni.previewImage({ current: url, urls: [url] }); |
| | | // #ifdef H5 |
| | | window.open(url, "_blank"); |
| | | // #endif |
| | | // #ifndef H5 |
| | | uni.showLoading({ title: "æå¼ä¸â¦", mask: true }); |
| | | uni.downloadFile({ |
| | | url, |
| | | success: result => uni.openDocument({ |
| | | filePath: result.tempFilePath, |
| | | showMenu: true, |
| | | fail: () => uni.setClipboardData({ data: url, success: () => toast("æä»¶é¾æ¥å·²å¤å¶") }) |
| | | }), |
| | | fail: () => uni.setClipboardData({ data: url, success: () => toast("æä»¶é¾æ¥å·²å¤å¶") }), |
| | | complete: () => uni.hideLoading() |
| | | }); |
| | | // #endif |
| | | } |
| | | async function approve() { |
| | | if (!state.accessToken || !await confirm("ç¡®è®¤å®¡æ ¸éè¿è¯¥éè´§ç³è¯·ï¼")) return; |
| | | try { await approveReturn(state.accessToken, id.value); toast("å®¡æ ¸éè¿ï¼å·²åæ¥MESå建éè´§å"); await load(); } |
| | |
| | | <view class="detail-row"><text>å®ééé¢ï¼å
ï¼</text><text>{{ money(data.actualRefundPrice) }}</text></view> |
| | | <view class="detail-row"><text>MESéè´§åå·</text><text>{{ data.mesReturnCode || "--" }}</text></view> |
| | | <view class="detail-row"><text>è´¨æ£ç»æ</text><text>{{ data.qualityResult || "--" }}</text></view> |
| | | <view class="detail-row"><text>è´¨æ£æ¥å</text><text :class="{ link: data.qualityReportUrl }" @click="previewReport">{{ data.qualityReportUrl ? "æ¥çæ¥å" : "--" }}</text></view> |
| | | |
| | | <view class="detail-row"><text>å
¥åºæ¶é´</text><text>{{ dateTime(data.returnInboundTime) }}</text></view> |
| | | <view class="detail-row"><text>鿬¾æ¶é´</text><text>{{ dateTime(data.refundTime) }}</text></view> |
| | | <view class="detail-row"><text>夿³¨</text><text>{{ data.remark || "--" }}</text></view> |
| | |
| | | import { getHrmUserList, type UserOption } from "../../../api/hrm"; |
| | | import { useAuth } from "../../../stores/auth"; |
| | | import PickerSheet from "../../mes/process-design/components/PickerSheet.vue"; |
| | | import AftersalesAttachmentField from "../AftersalesAttachmentField.vue"; |
| | | import { ISSUE_TYPES, SEVERITIES, TICKET_STATUS, VISIT_TYPES, confirm, dateTime, labelOf, toast } from "../shared"; |
| | | |
| | | const { hasPermission, state } = useAuth(); |
| | |
| | | <view class="detail-row"><text>é®é¢æè¿°</text><text>{{ ticket.issueDescription || "--" }}</text></view><view class="detail-row"><text>è´è´£äºº</text><text>{{ ticket.ownerUserName || "--" }}</text></view><view class="detail-row"><text>å¤ç人</text><text>{{ ticket.handlerUserName || "--" }}</text></view> |
| | | <view class="detail-row"><text>ç°åºç¾å°</text><text>{{ ticket.signInTime ? dateTime(ticket.signInTime) : "æªç¾å°" }}</text></view><view class="detail-row"><text>ç¾å°å°å</text><text>{{ ticket.signInAddress || "--" }}</text></view> |
| | | <view class="detail-row"><text>åè®¿ç¶æ</text><text>{{ ticket.visitStatus === 1 ? "å·²å访éè¿" : "å¾
å访" }}</text></view><view class="detail-row"><text>é®é¢æ²æ·</text><text>{{ ticket.problemNo || "--" }}</text></view><view class="detail-row"><text>å®ç»æ¶é´</text><text>{{ dateTime(ticket.resolvedTime) }}</text></view><view class="detail-row"><text>å
³éæ¶é´</text><text>{{ dateTime(ticket.closedTime) }}</text></view><view class="detail-row"><text>夿³¨</text><text>{{ ticket.remark || "--" }}</text></view> |
| | | <AftersalesAttachmentField :model-value="ticket.blobIds" :files="ticket.attachmentList" readonly /> |
| | | </view> |
| | | |
| | | <text class="section-title">å·¥åæç»</text> |
| | |
| | | import { getBatchPage, type Batch } from "../../../api/wls"; |
| | | import { useAuth } from "../../../stores/auth"; |
| | | import PickerSheet from "../../mes/process-design/components/PickerSheet.vue"; |
| | | import AftersalesAttachmentField from "../AftersalesAttachmentField.vue"; |
| | | import { enqueueTicket, isNetworkError } from "../offline-queue"; |
| | | import { ISSUE_TYPES, SEVERITIES, labelOf, toast } from "../shared"; |
| | | |
| | |
| | | <view class="field"><text>é®é¢åç±»</text><input v-model="form.issueCategory" placeholder="å¦å¤è§ç¼ºé·ãåè½æ
é" /></view> |
| | | <view class="field"><text>严éç¨åº¦</text><view class="picker" @click="severityVisible = true"><text>{{ labelOf(SEVERITIES, form.severityLevel) }}</text><text>âº</text></view></view> |
| | | <view class="field column"><text>夿³¨</text><textarea v-model="form.remark" placeholder="请è¾å
¥å¤æ³¨" /></view> |
| | | <AftersalesAttachmentField v-model="form.blobIds" v-model:files="form.attachmentList" :token="state.accessToken" /> |
| | | </view> |
| | | <text class="section-title">å·¥åæç»</text> |
| | | <view class="form-card"> |
| | |
| | | * |
| | | * ç¨äºãåèµ·æµç¨ãçèç¹é¢æµ ä¸ã审æ¹è¯¦æ
ãçå®¡æ¹æµã |
| | | * ç§»å¨ç«¯ä¸æ¸²æ bpmn-js æµç¨å¾ï¼è¿éç¨ç«åèç¹æµæ¿ä»£ï¼ä¿¡æ¯ä¸ web 端ä¸è´ï¼ |
| | | * èç¹ç±»å徿 + èç¹ç¶æè² + 人å + æ¶é´ + å®¡æ¹æè§/éä»¶ã |
| | | * èç¹ç±»å徿 + èç¹ç¶æè² + 人å + æ¶é´ + å®¡æ¹æè§ã |
| | | */ |
| | | import { |
| | | CANDIDATE_STRATEGY_START_USER_SELECT, |
| | |
| | | return ""; |
| | | } |
| | | |
| | | /** å岿件å¼ä»
ç¨äºåªè¯»å±ç¤ºï¼App ç«¯ä¸æä¾éä»¶ä¸ä¼ ã */ |
| | | function fileList(field: FormField) { |
| | | const value = getValue(field); |
| | | if (!Array.isArray(value)) return []; |
| | | return value.map(item => |
| | | typeof item === "string" ? { name: item, url: item } : item, |
| | | ); |
| | | } |
| | | |
| | | /** åªè¯»å±ç¤ºçææ¬ */ |
| | | function readonlyText(field: FormField) { |
| | | const value = getValue(field); |
| | |
| | | > |
| | | <text class="picker-text">{{ pickerText(field) }}</text> |
| | | <text class="arrow">âº</text> |
| | | </view> |
| | | |
| | | <!-- å¾ç / æä»¶ï¼ç±ç¶é¡µé¢æ³¨å
¥ä¸ä¼ é»è¾ï¼è¿éå±ç¤ºå·²ä¸ä¼ å表 --> |
| | | <view v-else-if="field.kind === 'image' || field.kind === 'file'" class="upload"> |
| | | <view v-for="(file, index) in fileList(field)" :key="index" class="upload-item"> |
| | | <text class="upload-name">{{ file.name || `æä»¶${index + 1}` }}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- å
åºï¼ä¸æ¯æçç»ä»¶æææ¬å½å
¥ï¼ä¿è¯å¼è½å¡«ä¸ --> |
| | |
| | | |
| | | /** ååæ®µæ ¡éªï¼è¿åéè¯¯ææ¡ï¼æ éè¿å ""ï¼ */ |
| | | export function validateField(field: FormField, raw: any): string { |
| | | // App ç«¯ä¸æä¾éä»¶ä¸ä¼ /ä¸è½½ï¼å¾çãæä»¶ç±»åæ®µå¨ App å
ä¸å䏿 ¡éªï¼é¿å
å¡ä½æäº¤ |
| | | if (field.kind === "image" || field.kind === "file") return ""; |
| | | const value = raw; |
| | | const empty = |
| | | value === undefined || |
| | |
| | | (Array.isArray(value) && value.length === 0); |
| | | |
| | | if (field.required && empty) { |
| | | return field.kind === "image" || field.kind === "file" |
| | | ? `请ä¸ä¼ ${field.title}` |
| | | : `请填å${field.title}`; |
| | | return `请填å${field.title}`; |
| | | } |
| | | if (empty) return ""; |
| | | |
| | |
| | | transferBusiness, |
| | | updateBusinessStatus, |
| | | updatePermission, |
| | | uploadCrmFile, |
| | | type Business, |
| | | type CrmContact, |
| | | type CrmContract, |
| | |
| | | const activeTab = ref("basic"); |
| | | const loading = ref(false); |
| | | const saving = ref(false); |
| | | const uploading = ref(false); |
| | | const followUps = ref<FollowUpRecord[]>([]); |
| | | const contacts = ref<CrmContact[]>([]); |
| | | const contracts = ref<CrmContract[]>([]); |
| | |
| | | uni.showToast({ title: "ç¶æå·²æ´æ°", icon: "success" }); |
| | | loadDetail(); |
| | | } |
| | | async function chooseFollowImage() { |
| | | if (!state.accessToken || uploading.value) return; |
| | | uni.chooseImage({ |
| | | count: 9 - followForm.picUrls.length, |
| | | success: async result => { |
| | | uploading.value = true; |
| | | try { |
| | | for (const path of result.tempFilePaths) { |
| | | const file = await uploadCrmFile(state.accessToken!, path); |
| | | const url = file.previewURL || file.url; |
| | | if (url) followForm.picUrls.push(url); |
| | | } |
| | | } catch (error) { |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : "å¾çä¸ä¼ 失败", |
| | | icon: "none", |
| | | }); |
| | | } finally { |
| | | uploading.value = false; |
| | | } |
| | | }, |
| | | }); |
| | | } |
| | | function removeImage(index: number) { |
| | | followForm.picUrls.splice(index, 1); |
| | | } |
| | | async function saveFollow() { |
| | | if ( |
| | | !state.accessToken || |
| | |
| | | </picker> |
| | | <view class="required-label">è·è¿å
容</view><textarea v-model="followForm.content" |
| | | placeholder="请è¾å
¥æ¬æ¬¡è·è¿å
容" ></textarea> |
| | | <view class="upload-head"><text>å¾ç</text><text @click="chooseFollowImage">ä¸ä¼ å¾ç</text></view> |
| | | <view v-if="followForm.picUrls.length" |
| | | class="image-list"> |
| | | <view v-for="(url,index) in followForm.picUrls" |
| | | :key="url" |
| | | class="image-item"> |
| | | <image :src="url" |
| | | mode="aspectFill" |
| | | @click="uni.previewImage({urls:followForm.picUrls,current:url})" /><text @click="removeImage(index)">Ã</text> |
| | | </view> |
| | | </view> |
| | | <button class="primary" |
| | | :loading="saving || uploading" |
| | | :loading="saving" |
| | | @click="saveFollow">ä¿åè·è¿</button> |
| | | </view> |
| | | <view v-for="item in followUps" |
| | |
| | | transformClue, |
| | | transferClue, |
| | | updatePermission, |
| | | uploadCrmFile, |
| | | type Clue, |
| | | type CrmPermission, |
| | | type DictData, |
| | |
| | | const memberUserId = ref<number>(); |
| | | const memberLevel = ref(2); |
| | | const saving = ref(false); |
| | | const uploading = ref(false); |
| | | const canUpdate = computed(() => hasPermission("crm:clue:update")); |
| | | const owner = computed(() => clue.value?.ownerUserId === state.user?.id); |
| | | const typeOptions = computed(() => |
| | |
| | | icon: "none", |
| | | }); |
| | | } |
| | | } |
| | | async function chooseFollowImage() { |
| | | if (!state.accessToken || uploading.value) return; |
| | | uni.chooseImage({ |
| | | count: 9 - followForm.picUrls.length, |
| | | success: async result => { |
| | | uploading.value = true; |
| | | try { |
| | | for (const path of result.tempFilePaths) { |
| | | const file = await uploadCrmFile(state.accessToken!, path); |
| | | const url = file.previewURL || file.url; |
| | | if (url) followForm.picUrls.push(url); |
| | | } |
| | | } catch (error) { |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : "å¾çä¸ä¼ 失败", |
| | | icon: "none", |
| | | }); |
| | | } finally { |
| | | uploading.value = false; |
| | | } |
| | | }, |
| | | }); |
| | | } |
| | | function removeImage(index: number) { |
| | | followForm.picUrls.splice(index, 1); |
| | | } |
| | | async function saveFollow() { |
| | | if ( |
| | |
| | | </picker> |
| | | <view class="required-label">è·è¿å
容</view><textarea v-model="followForm.content" |
| | | placeholder="请è¾å
¥æ¬æ¬¡è·è¿å
容" ></textarea> |
| | | <view class="upload-head"><text>å¾ç</text><text @click="chooseFollowImage">ä¸ä¼ å¾ç</text></view> |
| | | <view v-if="followForm.picUrls.length" |
| | | class="image-list"> |
| | | <view v-for="(url,index) in followForm.picUrls" |
| | | :key="url" |
| | | class="image-item"> |
| | | <image :src="url" |
| | | mode="aspectFill" |
| | | @click="uni.previewImage({urls:followForm.picUrls,current:url})" /><text @click="removeImage(index)">Ã</text> |
| | | </view> |
| | | </view> |
| | | <button class="primary" |
| | | :loading="saving || uploading" |
| | | :loading="saving" |
| | | @click="saveFollow">ä¿åè·è¿</button> |
| | | </view> |
| | | <view v-for="item in followUps" |
| | |
| | | submitContract, |
| | | transferContract, |
| | | updatePermission, |
| | | uploadCrmFile, |
| | | type ApproveProcess, |
| | | type CrmContract, |
| | | type CrmPermission, |
| | |
| | | const active = ref("basic"); |
| | | const loading = ref(false); |
| | | const saving = ref(false); |
| | | const uploading = ref(false); |
| | | const users = ref<UserSimple[]>([]); |
| | | const dicts = ref<DictData[]>([]); |
| | | const followUps = ref<FollowUpRecord[]>([]); |
| | |
| | | icon: "none", |
| | | }); |
| | | } |
| | | } |
| | | async function chooseImage() { |
| | | if (!state.accessToken || uploading.value) return; |
| | | uni.chooseImage({ |
| | | count: 9 - follow.picUrls.length, |
| | | success: async result => { |
| | | uploading.value = true; |
| | | try { |
| | | for (const path of result.tempFilePaths) { |
| | | const file = await uploadCrmFile(state.accessToken!, path); |
| | | const url = file.previewURL || file.url; |
| | | if (url) follow.picUrls.push(url); |
| | | } |
| | | } catch (error) { |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : "å¾çä¸ä¼ 失败", |
| | | icon: "none", |
| | | }); |
| | | } finally { |
| | | uploading.value = false; |
| | | } |
| | | }, |
| | | }); |
| | | } |
| | | function removeImage(index: number) { |
| | | follow.picUrls.splice(index, 1); |
| | | } |
| | | async function saveFollow() { |
| | | if ( |
| | |
| | | <view class="select">{{follow.nextTime||'è¯·éæ©æ¥æ'}}<text>âº</text></view> |
| | | </picker><text class="required-label">è·è¿å
容</text><textarea v-model="follow.content" |
| | | placeholder="请è¾å
¥æ¬æ¬¡è·è¿å
容" ></textarea> |
| | | <view class="upload-head"><text>å¾ç</text><text @click="chooseImage">ä¸ä¼ å¾ç</text></view> |
| | | <view class="images"> |
| | | <view v-for="(url,index) in follow.picUrls" |
| | | :key="url" |
| | | class="image"> |
| | | <image :src="url" |
| | | mode="aspectFill" |
| | | @click="uni.previewImage({urls:follow.picUrls,current:url})" /><text @click="removeImage(index)">Ã</text> |
| | | </view> |
| | | </view> |
| | | <button class="primary" |
| | | :loading="saving||uploading" |
| | | :loading="saving" |
| | | @click="saveFollow">ä¿åè·è¿</button> |
| | | </view> |
| | | <view v-for="item in followUps" |
| | |
| | | transferCustomer, |
| | | updateCustomerDealStatus, |
| | | updatePermission, |
| | | uploadCrmFile, |
| | | type Business, |
| | | type CrmContact, |
| | | type CrmContract, |
| | |
| | | const activeTab = ref("basic"); |
| | | const loading = ref(false); |
| | | const saving = ref(false); |
| | | const uploading = ref(false); |
| | | const contacts = ref<CrmContact[]>([]); |
| | | const businesses = ref<Business[]>([]); |
| | | const contracts = ref<CrmContract[]>([]); |
| | |
| | | }); |
| | | } |
| | | } |
| | | async function chooseImage() { |
| | | if (!state.accessToken || uploading.value) return; |
| | | uni.chooseImage({ |
| | | count: 9 - follow.picUrls.length, |
| | | success: async result => { |
| | | uploading.value = true; |
| | | try { |
| | | for (const path of result.tempFilePaths) { |
| | | const file = await uploadCrmFile(state.accessToken!, path); |
| | | const url = file.previewURL || file.url; |
| | | if (url) follow.picUrls.push(url); |
| | | } |
| | | } catch (error) { |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : "å¾çä¸ä¼ 失败", |
| | | icon: "none", |
| | | }); |
| | | } finally { |
| | | uploading.value = false; |
| | | } |
| | | }, |
| | | }); |
| | | } |
| | | function removeImage(index: number) { |
| | | follow.picUrls.splice(index, 1); |
| | | } |
| | | async function saveFollow() { |
| | | if ( |
| | | !state.accessToken || |
| | |
| | | <view class="select">{{follow.nextTime||'è¯·éæ©æ¥æ'}}<text>âº</text></view> |
| | | </picker><text class="required-label">è·è¿å
容</text><textarea v-model="follow.content" |
| | | placeholder="请è¾å
¥æ¬æ¬¡è·è¿å
容" ></textarea> |
| | | <view class="upload-head"><text>å¾ç</text><text @click="chooseImage">ä¸ä¼ å¾ç</text></view> |
| | | <view class="images"> |
| | | <view v-for="(url,index) in follow.picUrls" |
| | | :key="url" |
| | | class="image"> |
| | | <image :src="url" |
| | | mode="aspectFill" |
| | | @click="uni.previewImage({urls:follow.picUrls,current:url})" /><text @click="removeImage(index)">Ã</text> |
| | | </view> |
| | | </view> |
| | | <button class="primary" |
| | | :loading="saving||uploading" |
| | | :loading="saving" |
| | | @click="saveFollow">ä¿åè·è¿</button> |
| | | </view> |
| | | <view v-for="item in followUps" |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, ref } from 'vue'; import { onLoad, onShow } from '@dcloudio/uni-app'; import { getInvoice, submitInvoice, type CrmInvoice } from '../../../api/crm'; import { useAuth } from '../../../stores/auth'; |
| | | const {hasPermission,state}=useAuth();const id=ref(0),item=ref<CrmInvoice>(),loading=ref(false);const canUpdate=computed(()=>hasPermission('crm:invoice:update'));const money=(x?:number)=>x===undefined?'â':`Â¥${Number(x).toFixed(2)}`;const status=(x?:number)=>({0:'æªæäº¤',10:'审æ¹ä¸',20:'审æ¹éè¿',30:'审æ¹ä¸éè¿',40:'已忶',50:'å·²ä½åº'} as Record<number,string>)[x??0]||'æªç¥ç¶æ';async function load(){if(!state.accessToken||!id.value)return;loading.value=true;try{item.value=await getInvoice(state.accessToken,id.value);}catch(error){uni.showToast({title:error instanceof Error?error.message:'详æ
å 载失败',icon:'none'});}finally{loading.value=false;}}async function submit(){if(!state.accessToken)return;try{await submitInvoice(state.accessToken,id.value);uni.showToast({title:'å·²æäº¤å®¡æ ¸',icon:'success'});load();}catch(error){uni.showToast({title:error instanceof Error?error.message:'æäº¤å¤±è´¥',icon:'none'});}}onLoad(q=>{id.value=Number(q.id);load();});onShow(load); |
| | | </script><template><view class="page"><view v-if="loading" class="state">å è½½ä¸...</view><template v-else-if="item"><view class="summary"><view><text>{{item.no||'å¼ç¥¨ç³è¯·'}}</text><text>{{item.customerName||'â'}} · {{money(item.price)}}</text></view><text>{{status(item.auditStatus)}}</text></view><view class="toolbar"><button v-if="canUpdate" @click="uni.navigateTo({url:`/pages/crm/invoice/form?id=${id}`})">ç¼è¾</button><button v-if="canUpdate&&item.auditStatus===0" @click="submit">æäº¤å®¡æ ¸</button></view><view class="card"><view v-for="row in [['客æ·',item.customerName],['åå',item.contract?.no||item.contract?.name],['è´è´£äºº',item.ownerUserName],['æå±é¨é¨',item.ownerUserDeptName],['å票类å',item.invoiceType],['å票å·ç ',item.invoiceNo],['å票æ¬å¤´',item.invoiceTitle],['å¼ç¥¨éé¢',money(item.price)],['å¼ç¥¨æ¥æ',item.invoiceTime?.slice(0,10)],['å©ä½å¯å款éé¢',money(item.remainingInvoicePrice)],['å建人',item.creatorName],['å建æ¶é´',item.createTime?.replace('T',' ').slice(0,16)]]" :key="row[0]" class="row"><text>{{row[0]}}</text><text>{{row[1]??'â'}}</text></view><view class="remark"><text>夿³¨</text><text>{{item.remark||'â'}}</text></view><view class="remark"><text>å票éä»¶</text><text v-if="item.attachmentList?.length">{{item.attachmentList.map(x=>x.name||'éä»¶').join('ã')}}</text><text v-else>æªä¸ä¼ </text></view></view></template><view v-else class="state">ææ å¼ç¥¨ç³è¯·æ°æ®</view></view></template><style lang="scss" scoped>.page{min-height:100vh;padding:24rpx 30rpx;background:#f6f7f9}.summary{display:flex;align-items:center;justify-content:space-between;padding:26rpx;color:#fff;background:var(--color-primary, #1769d8);border-radius:16rpx}.summary text{display:block}.summary view text:first-child{font-size:32rpx;font-weight:600}.summary view text:last-child{margin-top:8rpx;font-size:24rpx;color:#dbeaff}.summary>text{padding:5rpx 10rpx;font-size:21rpx;background:rgba(255,255,255,.18);border-radius:8rpx}.toolbar{display:flex;justify-content:flex-end;gap:16rpx;padding:18rpx 0}.toolbar button{margin:0;color:var(--color-primary, #1769d8);font-size:24rpx;background:var(--color-primary-soft, #f5f8ff);border:1rpx solid var(--color-primary-border, #8ab3ee)}.toolbar button::after{border:0}.card{background:#fff;border-radius:16rpx}.row{display:flex;justify-content:space-between;gap:24rpx;min-height:88rpx;align-items:center;padding:0 24rpx;border-bottom:1rpx solid #eef1f5}.row text:first-child,.remark>text:first-child{color:#718198;font-size:26rpx}.row text:last-child{color:#263a54;font-size:26rpx;text-align:right}.remark{padding:20rpx 24rpx;border-bottom:1rpx solid #eef1f5}.remark text{display:block}.remark text:last-child{margin-top:12rpx;color:#263a54;font-size:26rpx;line-height:1.6}.state{padding:80rpx 20rpx;color:#8795a8;font-size:26rpx;text-align:center}</style> |
| | | </script><template><view class="page"><view v-if="loading" class="state">å è½½ä¸...</view><template v-else-if="item"><view class="summary"><view><text>{{item.no||'å¼ç¥¨ç³è¯·'}}</text><text>{{item.customerName||'â'}} · {{money(item.price)}}</text></view><text>{{status(item.auditStatus)}}</text></view><view class="toolbar"><button v-if="canUpdate" @click="uni.navigateTo({url:`/pages/crm/invoice/form?id=${id}`})">ç¼è¾</button><button v-if="canUpdate&&item.auditStatus===0" @click="submit">æäº¤å®¡æ ¸</button></view><view class="card"><view v-for="row in [['客æ·',item.customerName],['åå',item.contract?.no||item.contract?.name],['è´è´£äºº',item.ownerUserName],['æå±é¨é¨',item.ownerUserDeptName],['å票类å',item.invoiceType],['å票å·ç ',item.invoiceNo],['å票æ¬å¤´',item.invoiceTitle],['å¼ç¥¨éé¢',money(item.price)],['å¼ç¥¨æ¥æ',item.invoiceTime?.slice(0,10)],['å©ä½å¯å款éé¢',money(item.remainingInvoicePrice)],['å建人',item.creatorName],['å建æ¶é´',item.createTime?.replace('T',' ').slice(0,16)]]" :key="row[0]" class="row"><text>{{row[0]}}</text><text>{{row[1]??'â'}}</text></view><view class="remark"><text>夿³¨</text><text>{{item.remark||'â'}}</text></view></view></template><view v-else class="state">ææ å¼ç¥¨ç³è¯·æ°æ®</view></view></template><style lang="scss" scoped>.page{min-height:100vh;padding:24rpx 30rpx;background:#f6f7f9}.summary{display:flex;align-items:center;justify-content:space-between;padding:26rpx;color:#fff;background:var(--color-primary, #1769d8);border-radius:16rpx}.summary text{display:block}.summary view text:first-child{font-size:32rpx;font-weight:600}.summary view text:last-child{margin-top:8rpx;font-size:24rpx;color:#dbeaff}.summary>text{padding:5rpx 10rpx;font-size:21rpx;background:rgba(255,255,255,.18);border-radius:8rpx}.toolbar{display:flex;justify-content:flex-end;gap:16rpx;padding:18rpx 0}.toolbar button{margin:0;color:var(--color-primary, #1769d8);font-size:24rpx;background:var(--color-primary-soft, #f5f8ff);border:1rpx solid var(--color-primary-border, #8ab3ee)}.toolbar button::after{border:0}.card{background:#fff;border-radius:16rpx}.row{display:flex;justify-content:space-between;gap:24rpx;min-height:88rpx;align-items:center;padding:0 24rpx;border-bottom:1rpx solid #eef1f5}.row text:first-child,.remark>text:first-child{color:#718198;font-size:26rpx}.row text:last-child{color:#263a54;font-size:26rpx;text-align:right}.remark{padding:20rpx 24rpx;border-bottom:1rpx solid #eef1f5}.remark text{display:block}.remark text:last-child{margin-top:12rpx;color:#263a54;font-size:26rpx;line-height:1.6}.state{padding:80rpx 20rpx;color:#8795a8;font-size:26rpx;text-align:center}</style> |
| | |
| | | class="row" |
| | | @click="goDetail(item.id)"> |
| | | <view class="head"><text class="name">{{item.no||'å¾
çæå¼ç¥¨ç¼å·'}}</text><text :class="['tag', statusClass(item.auditStatus)]">{{status(item.auditStatus)}}</text></view> |
| | | <view class="grid"><text>客æ·ï¼{{item.customerName||'â'}}</text><text>ååï¼{{item.contract?.no||item.contract?.name||'â'}}</text><text>å票类åï¼{{item.invoiceType===undefined?'â':item.invoiceType}}</text><text>å票å·ç ï¼{{item.invoiceNo||'â'}}</text><text>å票æ¬å¤´ï¼{{item.invoiceTitle||'â'}}</text><text>å¼ç¥¨éé¢ï¼{{money(item.price)}}</text><text>å¼ç¥¨æ¥æï¼{{item.invoiceTime?.slice(0,10)||'â'}}</text><text>å©ä½å¯å¼ç¥¨ï¼{{money(item.remainingInvoicePrice)}}</text><text>è´è´£äººï¼{{item.ownerUserName||'â'}}</text><text>æå±é¨é¨ï¼{{item.ownerUserDeptName||'â'}}</text><text>å建人ï¼{{item.creatorName||'â'}}</text><text>å建æ¶é´ï¼{{item.createTime?.replace('T',' ').slice(0,16)||'â'}}</text><text>æ´æ°æ¶é´ï¼{{item.updateTime?.replace('T',' ').slice(0,16)||'â'}}</text><text>éä»¶ï¼{{item.hasAttachment?'å·²ä¸ä¼ ':'æªä¸ä¼ '}}</text><text class="full">夿³¨ï¼{{item.remark||'â'}}</text></view> |
| | | <view class="grid"><text>客æ·ï¼{{item.customerName||'â'}}</text><text>ååï¼{{item.contract?.no||item.contract?.name||'â'}}</text><text>å票类åï¼{{item.invoiceType===undefined?'â':item.invoiceType}}</text><text>å票å·ç ï¼{{item.invoiceNo||'â'}}</text><text>å票æ¬å¤´ï¼{{item.invoiceTitle||'â'}}</text><text>å¼ç¥¨éé¢ï¼{{money(item.price)}}</text><text>å¼ç¥¨æ¥æï¼{{item.invoiceTime?.slice(0,10)||'â'}}</text><text>å©ä½å¯å¼ç¥¨ï¼{{money(item.remainingInvoicePrice)}}</text><text>è´è´£äººï¼{{item.ownerUserName||'â'}}</text><text>æå±é¨é¨ï¼{{item.ownerUserDeptName||'â'}}</text><text>å建人ï¼{{item.creatorName||'â'}}</text><text>å建æ¶é´ï¼{{item.createTime?.replace('T',' ').slice(0,16)||'â'}}</text><text>æ´æ°æ¶é´ï¼{{item.updateTime?.replace('T',' ').slice(0,16)||'â'}}</text><text class="full">夿³¨ï¼{{item.remark||'â'}}</text></view> |
| | | <view class="actions"><button v-if="canUpdate" |
| | | @click.stop="goForm(item.id)">ç¼è¾</button><button v-if="canUpdate&&item.auditStatus===0" |
| | | @click.stop="submit(item)">æäº¤å®¡æ ¸</button><button v-if="canDelete" |
| | |
| | | <view class="head"><text class="name">{{x.name}}</text><text :class="['tag', quoteStatusClass(x.status)]">{{quoteStatusText(x.status)}}</text></view> |
| | | <view class="grid"><text>æ¥ä»·åå·ï¼{{x.no||'â'}}</text><text>客æ·ï¼{{x.customerName||'â'}}</text><text>åæºï¼{{x.businessName||'â'}}</text><text>è系人ï¼<text v-if="x.contactId" |
| | | class="link" |
| | | @click.stop="goContact(x.contactId)">{{x.contactName||'â'}}</text><text v-else>{{x.contactName||'â'}}</text></text><text>æ¥ä»·éé¢ï¼{{money(x.totalPrice)}}</text><text>äº§åæ»ä»·ï¼{{money(x.totalProductPrice)}}</text><text>æ´åææ£ï¼{{x.discountPercent===undefined?'â':`${x.discountPercent}%`}}</text><text>ç¨çï¼{{x.taxRate===undefined?'â':`${x.taxRate}%`}}</text><text>æ¥ä»·æ¥æï¼{{x.quotationTime?.slice(0,10)||'â'}}</text><text>æææè³ï¼{{x.validUntil?.slice(0,10)||'â'}}</text><text>è´è´£äººï¼{{x.ownerUserName||'â'}}</text><text>å建人ï¼{{x.creatorName||'â'}}</text><text>å建æ¶é´ï¼{{x.createTime?.replace('T',' ').slice(0,16)||'â'}}</text><text>æ´æ°æ¶é´ï¼{{x.updateTime?.replace('T',' ').slice(0,16)||'â'}}</text><text>å
³èååï¼{{x.contractNo||'â'}}</text><text>éä»¶ï¼{{x.attachmentList?.length?'å·²ä¸ä¼ ':'æªä¸ä¼ '}}</text><text class="full">夿³¨ï¼{{x.remark||'â'}}</text></view> |
| | | @click.stop="goContact(x.contactId)">{{x.contactName||'â'}}</text><text v-else>{{x.contactName||'â'}}</text></text><text>æ¥ä»·éé¢ï¼{{money(x.totalPrice)}}</text><text>äº§åæ»ä»·ï¼{{money(x.totalProductPrice)}}</text><text>æ´åææ£ï¼{{x.discountPercent===undefined?'â':`${x.discountPercent}%`}}</text><text>ç¨çï¼{{x.taxRate===undefined?'â':`${x.taxRate}%`}}</text><text>æ¥ä»·æ¥æï¼{{x.quotationTime?.slice(0,10)||'â'}}</text><text>æææè³ï¼{{x.validUntil?.slice(0,10)||'â'}}</text><text>è´è´£äººï¼{{x.ownerUserName||'â'}}</text><text>å建人ï¼{{x.creatorName||'â'}}</text><text>å建æ¶é´ï¼{{x.createTime?.replace('T',' ').slice(0,16)||'â'}}</text><text>æ´æ°æ¶é´ï¼{{x.updateTime?.replace('T',' ').slice(0,16)||'â'}}</text><text>å
³èååï¼{{x.contractNo||'â'}}</text><text class="full">夿³¨ï¼{{x.remark||'â'}}</text></view> |
| | | <view class="actions"><button v-if="canUpdate&&x.status===0" |
| | | @click.stop="form(x.id)">ç¼è¾</button><button v-if="canUpdate&&x.status===0&&!x.contractId" |
| | | @click.stop="convert(x)">转åå</button><button v-if="canDelete&&x.status===0" |
| | |
| | | <script setup lang="ts"> |
| | | import{computed,ref}from"vue";import{onLoad}from"@dcloudio/uni-app";import{createAttendanceException,getAttendanceException,getHrmUserList,type AttendanceException,type UserOption}from"../../../api/hrm";import{useAuth}from"../../../stores/auth";import PickerSheet from"../../mes/process-design/components/PickerSheet.vue";import HrmAttachmentField from"../HrmAttachmentField.vue";import{toast}from"../shared"; |
| | | import{computed,ref}from"vue";import{onLoad}from"@dcloudio/uni-app";import{createAttendanceException,getAttendanceException,getHrmUserList,type AttendanceException,type UserOption}from"../../../api/hrm";import{useAuth}from"../../../stores/auth";import PickerSheet from"../../mes/process-design/components/PickerSheet.vue";import{toast}from"../shared"; |
| | | const{state}=useAuth();const form=ref<AttendanceException>({exceptionType:1,blobIds:[]}),users=ref<UserOption[]>([]),userVisible=ref(false),typeVisible=ref(false),readonly=ref(false),submitting=ref(false);const types=[{key:1,label:"è¡¥ç¾å°"},{key:2,label:"è¡¥ç¾é"},{key:3,label:"è¿å°ç³è¯"},{key:4,label:"æ©éç³è¯"},{key:5,label:"缺å¤ç³è¯"},{key:6,label:"å
¶ä»"}];const userName=computed(()=>users.value.find(x=>x.id===form.value.userId)?.nickname||form.value.userName||"è¯·éæ©åå·¥"); |
| | | async function save(){if(!state.accessToken||readonly.value||!form.value.userId||!form.value.date||!form.value.reason)return toast("è¯·å®æ´å¡«ååå·¥ãæ¥æåç³è¯·åå ");submitting.value=true;try{await createAttendanceException(state.accessToken,form.value);toast("ä¿åæå");setTimeout(()=>uni.navigateBack(),400);}catch(e){toast(e instanceof Error?e.message:"ä¿å失败");}finally{submitting.value=false;}} |
| | | onLoad(async q=>{if(!state.accessToken)return;readonly.value=q?.detail==='1';try{users.value=await getHrmUserList(state.accessToken);if(q?.id)form.value=await getAttendanceException(state.accessToken,Number(q.id));}catch(e){toast(e instanceof Error?e.message:"å 载失败");}}); |
| | | </script><template><view class="page"><view class="form-card"><view class="field"><text class="required">ç³è¯·äºº</text><view class="picker" @click="!readonly&&(userVisible=true)"><text>{{userName}}</text><text>âº</text></view></view><view class="field"><text class="required">å¼å¸¸æ¥æ</text><input v-model="form.date" :disabled="readonly" placeholder="YYYY-MM-DD"/></view><view class="field"><text>å¼å¸¸ç±»å</text><view class="picker" @click="!readonly&&(typeVisible=true)"><text>{{types.find(x=>x.key===form.exceptionType)?.label||'è¯·éæ©'}}</text><text>âº</text></view></view><view class="field column"><text class="required">ç³è¯·åå </text><textarea v-model="form.reason" :disabled="readonly" placeholder="请è¾å
¥åå "/></view><view class="field column"><text>夿³¨</text><textarea v-model="form.remark" :disabled="readonly"/></view><HrmAttachmentField v-model="form.blobIds" v-model:files="form.attachmentList" :token="state.accessToken" :readonly="readonly"/></view><button v-if="!readonly" class="save" :loading="submitting" @click="save">ä¿åè稿</button><PickerSheet v-model:show="userVisible" title="éæ©åå·¥" :items="users.map(x=>({key:x.id,label:x.nickname||x.username||String(x.id),sub:x.deptName}))" :selected-keys="form.userId?[form.userId]:[]" @confirm="k=>form.userId=Number(k[0])"/><PickerSheet v-model:show="typeVisible" title="å¼å¸¸ç±»å" :items="types" :selected-keys="form.exceptionType?[form.exceptionType]:[]" :searchable="false" @confirm="k=>form.exceptionType=Number(k[0])"/></view></template><style scoped lang="scss">@import "../form.scss";.field input{text-align:right}</style> |
| | | </script><template><view class="page"><view class="form-card"><view class="field"><text class="required">ç³è¯·äºº</text><view class="picker" @click="!readonly&&(userVisible=true)"><text>{{userName}}</text><text>âº</text></view></view><view class="field"><text class="required">å¼å¸¸æ¥æ</text><input v-model="form.date" :disabled="readonly" placeholder="YYYY-MM-DD"/></view><view class="field"><text>å¼å¸¸ç±»å</text><view class="picker" @click="!readonly&&(typeVisible=true)"><text>{{types.find(x=>x.key===form.exceptionType)?.label||'è¯·éæ©'}}</text><text>âº</text></view></view><view class="field column"><text class="required">ç³è¯·åå </text><textarea v-model="form.reason" :disabled="readonly" placeholder="请è¾å
¥åå "/></view><view class="field column"><text>夿³¨</text><textarea v-model="form.remark" :disabled="readonly"/></view></view><button v-if="!readonly" class="save" :loading="submitting" @click="save">ä¿åè稿</button><PickerSheet v-model:show="userVisible" title="éæ©åå·¥" :items="users.map(x=>({key:x.id,label:x.nickname||x.username||String(x.id),sub:x.deptName}))" :selected-keys="form.userId?[form.userId]:[]" @confirm="k=>form.userId=Number(k[0])"/><PickerSheet v-model:show="typeVisible" title="å¼å¸¸ç±»å" :items="types" :selected-keys="form.exceptionType?[form.exceptionType]:[]" :searchable="false" @confirm="k=>form.exceptionType=Number(k[0])"/></view></template><style scoped lang="scss">@import "../form.scss";.field input{text-align:right}</style> |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, reactive, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import { createContract, getContract, getEmployeeSimpleList, getHrmDicts, updateContract, type EmployeeContract, type EmployeeSimple, type HrmAttachment } from "../../../api/hrm"; |
| | | import { createContract, getContract, getEmployeeSimpleList, getHrmDicts, updateContract, type EmployeeContract, type EmployeeSimple } from "../../../api/hrm"; |
| | | import type { DictData } from "../../../api/crm"; |
| | | import { useAuth } from "../../../stores/auth"; |
| | | import { HRM_DICT, label, options } from "../shared"; |
| | | import HrmAttachmentField from "../HrmAttachmentField.vue"; |
| | | |
| | | const { hasPermission, state } = useAuth(); |
| | | const id = ref<number>(); |
| | |
| | | const saving = ref(false); |
| | | const dicts = ref<DictData[]>([]); |
| | | const employees = ref<EmployeeSimple[]>([]); |
| | | const attachments = ref<HrmAttachment[]>([]); |
| | | const form = reactive<EmployeeContract>({ employeeId: undefined, contractType: 1, contractTermType: 1, startDate: "", blobIds: [] }); |
| | | const readonly = computed(() => mode.value === "detail"); |
| | | const canSave = computed(() => !readonly.value && (id.value |
| | |
| | | if (id.value) { |
| | | const detail = await getContract(state.accessToken, id.value); |
| | | Object.assign(form, detail, { blobIds: detail.attachmentList?.map(item => item.id) || detail.blobIds || [] }); |
| | | attachments.value = detail.attachmentList || []; |
| | | } else if (renewFrom.value) { |
| | | const old = await getContract(state.accessToken, renewFrom.value); |
| | | Object.assign(form, { |
| | |
| | | <view class="field"><text>è¯ç¨æç»æ</text><picker v-if="!readonly" mode="date" :value="form.probationEndDate" @change="form.probationEndDate=$event.detail.value"><view class="picker"><text>{{ form.probationEndDate || "è¯·éæ©æ¥æ" }}</text><text>âº</text></view></picker><text v-else class="value">{{ form.probationEndDate || "--" }}</text></view> |
| | | <view class="field"><text>è¯ç¨æå·¥èµ</text><input v-model.number="form.probationSalary" :disabled="readonly" type="digit" placeholder="请è¾å
¥éé¢" /></view> |
| | | <view class="field"><text>转æ£å·¥èµ</text><input v-model.number="form.regularSalary" :disabled="readonly" type="digit" placeholder="请è¾å
¥éé¢" /></view> |
| | | <HrmAttachmentField v-model="form.blobIds" v-model:files="attachments" :token="state.accessToken" :readonly="readonly" /> |
| | | <view class="field column"><text>夿³¨</text><textarea v-model="form.remark" :disabled="readonly" placeholder="请è¾å
¥å¤æ³¨" /></view> |
| | | </view> |
| | | <button v-if="canSave" class="save" :loading="saving" @click="save">ä¿å</button> |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, reactive, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import { createLeave, getAutoApprovalPolicy, getHrmDicts, getLeave, getLeaveProcesses, submitLeave, updateLeave, type ApproveProcess, type HrmAttachment, type Leave } from "../../../api/hrm"; |
| | | import { createLeave, getAutoApprovalPolicy, getHrmDicts, getLeave, getLeaveProcesses, submitLeave, updateLeave, type ApproveProcess, type Leave } from "../../../api/hrm"; |
| | | import type { DictData } from "../../../api/crm"; |
| | | import { useAuth } from "../../../stores/auth"; |
| | | import { HRM_DICT, label, options, setDateTime } from "../shared"; |
| | | import HrmAttachmentField from "../HrmAttachmentField.vue"; |
| | | |
| | | const { hasPermission, state } = useAuth(); |
| | | const id = ref<number>(); |
| | |
| | | const saving = ref(false); |
| | | const submitting = ref(false); |
| | | const dicts = ref<DictData[]>([]); |
| | | const attachments = ref<HrmAttachment[]>([]); |
| | | const form = reactive<Leave>({ userId: undefined, leaveType: undefined, startTime: "", endTime: "", duration: 0.5, reason: "", blobIds: [] }); |
| | | const readonly = computed(() => mode.value === "detail"); |
| | | const canSave = computed(() => !readonly.value && (id.value ? hasPermission("hrm:leave:update") : hasPermission("hrm:leave:create"))); |
| | |
| | | if (!id.value) throw new Error("è稿ä¿å失败"); |
| | | const latest = await getLeave(state.accessToken, id.value); |
| | | Object.assign(form, latest, { blobIds: latest.attachmentList?.map(item => item.id) || latest.blobIds || [] }); |
| | | attachments.value = latest.attachmentList || []; |
| | | return latest; |
| | | } |
| | | |
| | |
| | | return setTimeout(() => uni.navigateBack(), 500); |
| | | } |
| | | Object.assign(form, detail, { blobIds: detail.attachmentList?.map(item => item.id) || detail.blobIds || [] }); |
| | | attachments.value = detail.attachmentList || []; |
| | | } |
| | | else { form.userId = state.user?.id; form.userName = state.user?.nickname || state.user?.username; form.deptName = state.user?.dept?.name; } |
| | | } catch (error) { |
| | |
| | | </view> |
| | | <view class="field"><text>è¯·åæ¶é¿ <text class="required">*</text></text><input v-model.number="form.duration" :disabled="readonly" type="digit" placeholder="åä½ï¼å¤©" /></view> |
| | | <view class="field column"><text>请ååå <text class="required">*</text></text><textarea v-model="form.reason" :disabled="readonly" maxlength="500" placeholder="请è¾å
¥è¯·ååå " /></view> |
| | | <HrmAttachmentField class="leave-attachment" v-model="form.blobIds" v-model:files="attachments" :token="state.accessToken" :readonly="readonly" /> |
| | | <view class="field column"><text>夿³¨</text><textarea v-model="form.remark" :disabled="readonly" placeholder="请è¾å
¥å¤æ³¨" /></view> |
| | | </view> |
| | | <view v-if="canSave" class="form-actions"> |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, reactive, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import { createResignation, getEmployeeSimpleList, getHrmDicts, getResignation, updateResignation, type EmployeeSimple, type HrmAttachment, type Resignation } from "../../../api/hrm"; |
| | | import { createResignation, getEmployeeSimpleList, getHrmDicts, getResignation, updateResignation, type EmployeeSimple, type Resignation } from "../../../api/hrm"; |
| | | import type { DictData } from "../../../api/crm"; |
| | | import { useAuth } from "../../../stores/auth"; |
| | | import { HRM_DICT, label, options } from "../shared"; |
| | | import HrmAttachmentField from "../HrmAttachmentField.vue"; |
| | | import PickerSheet from "../../mes/process-design/components/PickerSheet.vue"; |
| | | |
| | | const { hasPermission, state } = useAuth(); |
| | | const id = ref<number>(); const mode = ref("edit"); const saving = ref(false); |
| | | const handoverVisible = ref(false); |
| | | const dicts = ref<DictData[]>([]); const employees = ref<EmployeeSimple[]>([]); const attachments = ref<HrmAttachment[]>([]); |
| | | const dicts = ref<DictData[]>([]); const employees = ref<EmployeeSimple[]>([]); |
| | | const form = reactive<Resignation>({ userId: undefined, resignationType: undefined, expectedDate: "", reason: "", handoverUserId: undefined, handoverRemark: "", blobIds: [], remark: "" }); |
| | | const readonly = computed(() => mode.value === "detail"); |
| | | const canSave = computed(() => !readonly.value && (id.value ? hasPermission("hrm:resignation:update") : hasPermission("hrm:resignation:create"))); |
| | |
| | | return setTimeout(() => uni.navigateBack(), 500); |
| | | } |
| | | Object.assign(form, detail, { blobIds: detail.attachmentList?.map(item => item.id) || detail.blobIds || [] }); |
| | | attachments.value = detail.attachmentList || []; |
| | | } |
| | | else { form.userId = state.user?.id; form.userName = state.user?.nickname || state.user?.username; form.deptName = state.user?.dept?.name; } |
| | | } catch (cause) { uni.showToast({ title: cause instanceof Error ? cause.message : "æ°æ®å 载失败", icon: "none" }); } |
| | |
| | | <view class="field"><text>交æ¥äºº</text><view v-if="!readonly" class="picker" @click="handoverVisible=true"><text>{{ handoverText }}</text><text>âº</text></view><text v-else class="value">{{ handoverText }}</text></view> |
| | | <view class="field column"><text>离èåå <text class="required">*</text></text><textarea v-model="form.reason" :disabled="readonly" maxlength="500" placeholder="请è¾å
¥ç¦»èåå " /></view> |
| | | <view class="field column"><text>交æ¥è¯´æ</text><textarea v-model="form.handoverRemark" :disabled="readonly" maxlength="500" placeholder="请è¾å
¥äº¤æ¥è¯´æ" /></view> |
| | | <HrmAttachmentField v-model="form.blobIds" v-model:files="attachments" :token="state.accessToken" :readonly="readonly" /> |
| | | <view class="field column"><text>夿³¨</text><textarea v-model="form.remark" :disabled="readonly" placeholder="请è¾å
¥å¤æ³¨" /></view> |
| | | </view><button v-if="canSave" class="save" :loading="saving" @click="save">ä¿åè稿</button><button v-else-if="readonly&&form.status===0&&hasPermission('hrm:resignation:update')" class="save" @click="mode='edit'">ç¼è¾</button><PickerSheet v-model:show="handoverVisible" title="éæ©äº¤æ¥äºº" :items="employees.map(item=>({key:item.id||0,label:item.name||'--',sub:item.employeeNo}))" :selected-keys="form.handoverUserId?[form.handoverUserId]:[]" search-placeholder="æç´¢å§åæå·¥å·" @confirm="selectHandover" /></view></template> |
| | | <style lang="scss" scoped>@import "../form.scss";</style> |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, reactive, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import { createTransfer, getHrmDeptList, getHrmDicts, getHrmPostList, getHrmUserList, getTransfer, updateTransfer, type HrmAttachment, type SimpleOption, type Transfer, type UserOption } from "../../../api/hrm"; |
| | | import { createTransfer, getHrmDeptList, getHrmDicts, getHrmPostList, getHrmUserList, getTransfer, updateTransfer, type SimpleOption, type Transfer, type UserOption } from "../../../api/hrm"; |
| | | import type { DeptSimple, DictData } from "../../../api/crm"; |
| | | import { useAuth } from "../../../stores/auth"; |
| | | import { HRM_DICT, label, options } from "../shared"; |
| | | import HrmAttachmentField from "../HrmAttachmentField.vue"; |
| | | |
| | | const { hasPermission, state } = useAuth(); |
| | | const id = ref<number>(); const mode = ref("edit"); const saving = ref(false); |
| | | const dicts = ref<DictData[]>([]); const users = ref<UserOption[]>([]); const depts = ref<DeptSimple[]>([]); const posts = ref<SimpleOption[]>([]); |
| | | const attachments = ref<HrmAttachment[]>([]); |
| | | const form = reactive<Transfer>({ userId: undefined, currentDeptId: undefined, currentPostId: undefined, targetDeptId: undefined, targetPostId: undefined, transferType: undefined, effectiveDate: "", reason: "", blobIds: [] }); |
| | | const readonly = computed(() => mode.value === "detail"); |
| | | const canSave = computed(() => !readonly.value && (id.value ? hasPermission("hrm:transfer:update") : hasPermission("hrm:transfer:create"))); |
| | |
| | | return setTimeout(() => uni.navigateBack(), 500); |
| | | } |
| | | Object.assign(form, detail, { blobIds: detail.attachmentList?.map(item => item.id) || detail.blobIds || [] }); |
| | | attachments.value = detail.attachmentList || []; |
| | | } |
| | | else { |
| | | const current = userData.find(item => item.id === state.user?.id); |
| | |
| | | <view class="field"><text>è°å²ç±»å <text class="required">*</text></text><picker v-if="!readonly" :range="typeOptions" range-key="label" @change="form.transferType=Number(typeOptions[$event.detail.value].value)"><view class="picker"><text>{{ label(dicts,HRM_DICT.transferType,form.transferType,"è¯·éæ©è°å²ç±»å") }}</text><text>âº</text></view></picker><text v-else class="value">{{ label(dicts,HRM_DICT.transferType,form.transferType) }}</text></view> |
| | | <view class="field"><text>çææ¥æ <text class="required">*</text></text><picker v-if="!readonly" mode="date" :value="form.effectiveDate" @change="form.effectiveDate=$event.detail.value"><view class="picker"><text>{{ form.effectiveDate || "è¯·éæ©çææ¥æ" }}</text><text>âº</text></view></picker><text v-else class="value">{{ form.effectiveDate || "--" }}</text></view> |
| | | <view class="field column"><text>è°å²åå <text class="required">*</text></text><textarea v-model="form.reason" :disabled="readonly" maxlength="500" placeholder="请è¾å
¥è°å²åå " /></view> |
| | | <HrmAttachmentField v-model="form.blobIds" v-model:files="attachments" :token="state.accessToken" :readonly="readonly" /> |
| | | <view class="field column"><text>夿³¨</text><textarea v-model="form.remark" :disabled="readonly" placeholder="请è¾å
¥å¤æ³¨" /></view> |
| | | </view><button v-if="canSave" class="save" :loading="saving" @click="save">ä¿åè稿</button><button v-else-if="readonly&&form.status===0&&hasPermission('hrm:transfer:update')" class="save" @click="mode='edit'">ç¼è¾</button></view> |
| | | </template> |
| | |
| | | getProductSopListByItemId, |
| | | getProductBomListByItemId, |
| | | saveBatchConfig, |
| | | uploadMesFile, |
| | | updateProductSip, |
| | | updateProductSop, |
| | | updateMesItem, |
| | |
| | | const generating = ref(false); |
| | | const bomLoading = ref(false); |
| | | const batchSaving = ref(false); |
| | | const uploading = ref(false); |
| | | const selectorOpen = ref(false); |
| | | const mediaOpen = ref(false); |
| | | const mediaKind = ref<"sip" | "sop">("sip"); |
| | |
| | | uni.showToast({ title: "ä¿åæå", icon: "success" }); |
| | | await loadSub(); |
| | | } |
| | | function previewMediaImage() { |
| | | if (mediaForm.url) uni.previewImage({ urls: [mediaForm.url], current: mediaForm.url }); |
| | | } |
| | | function removeMediaImage() { |
| | | mediaForm.url = undefined; |
| | | } |
| | | async function chooseMediaImage() { |
| | | if (!state.accessToken || uploading.value) return; |
| | | uni.chooseImage({ |
| | | count: 1, |
| | | success: async result => { |
| | | uploading.value = true; |
| | | try { |
| | | const file = await uploadMesFile(state.accessToken!, result.tempFilePaths[0]); |
| | | mediaForm.url = file.previewURL || file.url; |
| | | uni.showToast({ title: "ä¸ä¼ æå", icon: "success" }); |
| | | } catch (error) { |
| | | uni.showToast({ title: error instanceof Error ? error.message : "å¾çä¸ä¼ 失败", icon: "none" }); |
| | | } finally { |
| | | uploading.value = false; |
| | | } |
| | | }, |
| | | }); |
| | | } |
| | | async function removeMedia(kind: "sip" | "sop", row: ProductMedia) { |
| | | if (!state.accessToken || !row.id || !canEditSub.value) return; |
| | | const ok = await new Promise<boolean>(resolve => uni.showModal({ title: "确认å é¤", content: "ç¡®å®å é¤è¯¥æ°æ®åï¼", success: res => resolve(res.confirm) })); |
| | |
| | | </view> |
| | | |
| | | <view v-if="selectorOpen" class="mask" @click="selectorOpen=false"><view class="modal" @click.stop><view class="modal-title">{{ selectedBom?.id ? 'ç¼è¾ BOM ç©æ' : 'æ·»å BOM ç©æ' }}</view><view class="selector-search"><input v-model="materialKeyword" placeholder="æç´¢ç©æåç§°" @confirm="loadMaterials" /><button @click="loadMaterials">æç´¢</button></view><view class="selector-layout"><scroll-view class="selector-tree" scroll-y><view v-for="cat in materialCategoryOptions" :key="cat.id || 0" :class="['cat', { active: cat.id === materialTypeId }]" @click="materialTypeId=cat.id;loadMaterials()">{{ cat.name }}</view></scroll-view><scroll-view class="selector-list" scroll-y><view v-for="row in materialRows" :key="row.id" :class="['material', { selected: row.id === bomForm.bomItemId }]" @click="chooseMaterial(row)"><text>{{ row.name }}</text><text>{{ [row.code,row.specification,row.unitMeasureName].filter(Boolean).join(' · ') || '--' }}</text></view><view v-if="!materialRows.length" class="state compact">ææ ç©æ</view></scroll-view></view><view class="field"><text>å·²éç©æ</text><text class="value">{{ bomForm.bomItemName || 'è¯·éæ© BOM ç©æ' }}</text></view><view class="field"><text>ç¨éæ¯ä¾ <text class="required">*</text></text><input :value="String(bomForm.quantity ?? '')" type="digit" placeholder="请è¾å
¥ç¨éæ¯ä¾" @input="bomForm.quantity=num($event.detail.value)" /></view><view class="field column"><text>夿³¨</text><textarea v-model="bomForm.remark" placeholder="请è¾å
¥å¤æ³¨" /></view><view class="modal-actions"><button class="ghost" @click="selectorOpen=false">åæ¶</button><button class="primary" @click="saveBom">ä¿å</button></view></view></view> |
| | | <view v-if="mediaOpen" class="mask" @click="mediaOpen=false"><view class="modal" @click.stop><view class="modal-title">{{ selectedMedia?.id ? 'ç¼è¾' : 'æ°å¢' }} {{ mediaKind === 'sip' ? 'SIP' : 'SOP' }}</view><view class="field"><text>æ é¢ <text class="required">*</text></text><input v-model="mediaForm.title" placeholder="请è¾å
¥æ é¢" /></view><view class="field"><text>å±ç¤ºé¡ºåº</text><input :value="String(mediaForm.sort ?? 0)" type="number" placeholder="请è¾å
¥å±ç¤ºé¡ºåº" @input="mediaForm.sort=Number($event.detail.value||0)" /></view><view class="field"><text>æå±å·¥åº</text><picker :range="processes" range-key="name" @change="selectProcess(Number($event.detail.value))"><view class="picker"><text>{{ mediaForm.processName || 'è¯·éæ©æå±å·¥åº' }}</text><text>âº</text></view></picker></view><view class="field column"><view class="upload-head"><text>å¾ç</text><button :loading="uploading" :disabled="uploading" @click="chooseMediaImage">{{ uploading ? 'ä¸ä¼ ä¸' : 'ä¸ä¼ å¾ç' }}</button></view><view v-if="mediaForm.url" class="upload-image"><image :src="mediaForm.url" mode="aspectFill" @click="previewMediaImage" /><text @click="removeMediaImage">å é¤</text></view><text v-else class="upload-empty">ææªä¸ä¼ å¾ç</text></view><view class="field column"><text>å
容说æ</text><textarea v-model="mediaForm.description" placeholder="请è¾å
¥è¯¦ç»æè¿°" /></view><view class="field column"><text>夿³¨</text><textarea v-model="mediaForm.remark" placeholder="请è¾å
¥å¤æ³¨" /></view><view class="modal-actions"><button class="ghost" @click="mediaOpen=false">åæ¶</button><button class="primary" @click="saveMedia">ä¿å</button></view></view></view> |
| | | <view v-if="mediaOpen" class="mask" @click="mediaOpen=false"><view class="modal" @click.stop><view class="modal-title">{{ selectedMedia?.id ? 'ç¼è¾' : 'æ°å¢' }} {{ mediaKind === 'sip' ? 'SIP' : 'SOP' }}</view><view class="field"><text>æ é¢ <text class="required">*</text></text><input v-model="mediaForm.title" placeholder="请è¾å
¥æ é¢" /></view><view class="field"><text>å±ç¤ºé¡ºåº</text><input :value="String(mediaForm.sort ?? 0)" type="number" placeholder="请è¾å
¥å±ç¤ºé¡ºåº" @input="mediaForm.sort=Number($event.detail.value||0)" /></view><view class="field"><text>æå±å·¥åº</text><picker :range="processes" range-key="name" @change="selectProcess(Number($event.detail.value))"><view class="picker"><text>{{ mediaForm.processName || 'è¯·éæ©æå±å·¥åº' }}</text><text>âº</text></view></picker></view><view class="field column"><text>å
容说æ</text><textarea v-model="mediaForm.description" placeholder="请è¾å
¥è¯¦ç»æè¿°" /></view><view class="field column"><text>夿³¨</text><textarea v-model="mediaForm.remark" placeholder="请è¾å
¥å¤æ³¨" /></view><view class="modal-actions"><button class="ghost" @click="mediaOpen=false">åæ¶</button><button class="primary" @click="saveMedia">ä¿å</button></view></view></view> |
| | | </view> |
| | | </template> |
| | | |
| | |
| | | return dictLabel(dicts.value, DICT.MES_PD_ARCHIVE_STATUS, status, "--"); |
| | | } |
| | | |
| | | /** é¢è§/ä¸è½½æä»¶ï¼H5 ç´æ¥æ°çªå£æå¼ï¼å°ç¨åº/App å
ä¸è½½åæå¼ææ¡£ */ |
| | | function openFile(url?: string) { |
| | | if (!url) { |
| | | toast("è¯¥èµæææ æä»¶"); |
| | | return; |
| | | } |
| | | // #ifdef H5 |
| | | window.open(url, "_blank"); |
| | | // #endif |
| | | // #ifndef H5 |
| | | uni.showLoading({ title: "æå¼ä¸â¦", mask: true }); |
| | | uni.downloadFile({ |
| | | url, |
| | | success: res => { |
| | | if (res.statusCode !== 200) { |
| | | toast("æä»¶ä¸è½½å¤±è´¥"); |
| | | return; |
| | | } |
| | | uni.openDocument({ |
| | | filePath: res.tempFilePath, |
| | | showMenu: true, |
| | | fail: () => { |
| | | // é¨åæ ¼å¼ï¼å¦ zipï¼æ æ³ç´æ¥æå¼ï¼éåå¤å¶é¾æ¥ |
| | | uni.setClipboardData({ data: url, success: () => toast("æä»¶é¾æ¥å·²å¤å¶") }); |
| | | }, |
| | | }); |
| | | }, |
| | | fail: () => { |
| | | uni.setClipboardData({ data: url, success: () => toast("æä»¶é¾æ¥å·²å¤å¶") }); |
| | | }, |
| | | complete: () => uni.hideLoading(), |
| | | }); |
| | | // #endif |
| | | } |
| | | |
| | | async function load() { |
| | | if (!state.accessToken || !productCode.value) return; |
| | |
| | | <text>æä»¶åï¼{{ doc.fileName || '--' }}</text> |
| | | <text>ä¸ä¼ 人ï¼{{ doc.uploadUserNickname || '--' }}</text> |
| | | <text>ä¸ä¼ æ¶é´ï¼{{ dateTimeText(doc.uploadTime) }}</text> |
| | | </view> |
| | | <view v-if="doc.fileUrl" class="sub-actions"> |
| | | <button @click="openFile(doc.fileUrl)">é¢è§ / ä¸è½½</button> |
| | | </view> |
| | | </view> |
| | | </template> |
| | |
| | | const docForm = ref<PdDocument>({ documentType: undefined, documentName: "", documentVersion: "", blobIds: [], remark: "" }); |
| | | const docEditingId = ref<number>(); |
| | | const docTypeVisible = ref(false); |
| | | const docAttachments = ref<Array<{ id?: number; name?: string; url?: string }>>([]); |
| | | |
| | | /* ---------- èµæå®¡æ ¸å表 ---------- */ |
| | | const audits = ref<PdDocumentAudit[]>([]); |
| | |
| | | docForm.value = row |
| | | ? { ...row, blobIds: row.blobIds ?? [] } |
| | | : { documentType: undefined, documentName: "", documentVersion: project.value.version || "", blobIds: [], remark: "" }; |
| | | docAttachments.value = (row?.attachmentList ?? []).map(item => ({ id: item.id, name: item.name, url: item.url })); |
| | | docVisible.value = true; |
| | | } |
| | | |
| | |
| | | ...docForm.value, |
| | | projectId: projectId.value, |
| | | blobIds: docForm.value.blobIds ?? [], |
| | | fileName: docAttachments.value[0]?.name, |
| | | fileUrl: docAttachments.value[0]?.url, |
| | | }; |
| | | if (docEditingId.value) { |
| | | await updatePdDocument(state.accessToken, { ...payload, id: docEditingId.value }); |
| | |
| | | } catch (error) { |
| | | toast(error instanceof Error ? error.message : "å é¤å¤±è´¥"); |
| | | } |
| | | } |
| | | |
| | | /** é¢è§èµææä»¶ï¼H5 æ°çªå£æå¼ï¼å°ç¨åº/App ä¸è½½åè°ç¨ç³»ç»æå¼ */ |
| | | function previewDoc(row: PdDocument) { |
| | | if (!row.fileUrl) { |
| | | toast("è¯¥èµæææ æä»¶"); |
| | | return; |
| | | } |
| | | const url = row.fileUrl; |
| | | // #ifdef H5 |
| | | window.open(url, "_blank"); |
| | | // #endif |
| | | // #ifndef H5 |
| | | uni.showLoading({ title: "æå¼ä¸â¦", mask: true }); |
| | | uni.downloadFile({ |
| | | url, |
| | | success: res => { |
| | | if (res.statusCode !== 200) { |
| | | toast("æä»¶ä¸è½½å¤±è´¥"); |
| | | return; |
| | | } |
| | | uni.openDocument({ |
| | | filePath: res.tempFilePath, |
| | | showMenu: true, |
| | | fail: () => { |
| | | uni.setClipboardData({ data: url, success: () => toast("æä»¶é¾æ¥å·²å¤å¶") }); |
| | | }, |
| | | }); |
| | | }, |
| | | fail: () => { |
| | | uni.setClipboardData({ data: url, success: () => toast("æä»¶é¾æ¥å·²å¤å¶") }); |
| | | }, |
| | | complete: () => uni.hideLoading(), |
| | | }); |
| | | // #endif |
| | | } |
| | | |
| | | /* ---------- èµæå®¡æ ¸ ---------- */ |
| | |
| | | <text>ä¸ä¼ æ¶é´ï¼{{ dateTimeText(row.uploadTime) }}</text> |
| | | </view> |
| | | <view class="actions"> |
| | | <button v-if="row.fileUrl" @click="previewDoc(row)">é¢è§</button> |
| | | <button v-if="docEditable" @click="openDocForm(row)">ç¼è¾</button> |
| | | <button v-if="docEditable" class="danger" @click="removeDoc(row)">å é¤</button> |
| | | </view> |
| | |
| | | type PurchaseKind, |
| | | type PurchaseRecord, |
| | | } from "../../../api/purchase"; |
| | | import type { StorageAttachment } from "../../../api/supplier"; |
| | | import { useAuth } from "../../../stores/auth"; |
| | | |
| | | const props = defineProps<{ kind: PurchaseKind }>(); |
| | |
| | | const status = (value?: number) => |
| | | ({ 10: "å¾
å®¡æ ¸", 15: "审æ¹ä¸", 20: "å·²å®¡æ ¸", 30: "å·²æ¤é" }[value || 0] || |
| | | "--"); |
| | | const attachmentName = (attachment: StorageAttachment) => |
| | | attachment.name || `éä»¶ #${attachment.id}`; |
| | | function preview(attachment: StorageAttachment) { |
| | | if (attachment.url) uni.previewImage({ urls: [attachment.url] }); |
| | | else uni.showToast({ title: "éä»¶å°åä¸å¯ç¨", icon: "none" }); |
| | | } |
| | | onLoad(async query => { |
| | | if (!state.accessToken || !query.id) return; |
| | | try { |
| | |
| | | <text v-if="item.remark">{{ item.remark }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="card"> |
| | | <text class="title">éä»¶</text> |
| | | <view v-if="record.attachmentList?.length"> |
| | | <text v-for="attachment in record.attachmentList" |
| | | :key="attachment.id" |
| | | class="attachment" |
| | | @click="preview(attachment)">{{ attachmentName(attachment) }}</text> |
| | | </view> |
| | | <text v-else |
| | | class="empty">ææ éä»¶</text> |
| | | </view> |
| | | </template> |
| | | <view v-else |
| | | class="state">å è½½ä¸...</view> |
| | |
| | | display: block; |
| | | margin: 8rpx 0; |
| | | } |
| | | .attachment { |
| | | display: block; |
| | | padding: 18rpx 0; |
| | | border-top: 1rpx solid #f2f3f5; |
| | | color: var(--color-primary, #165dff); |
| | | font-size: 25rpx; |
| | | } |
| | | |
| | | .empty, |
| | | .state { |
| | | display: block; |
| | |
| | | @click="go('detail',i.id)"> |
| | | <view class="card-head"> |
| | | <text class="no">{{ i.no || '--' }}</text> |
| | | <text :class="['attachment', { uploaded: i.hasAttachment || i.attachmentList?.length }]"> |
| | | {{ i.hasAttachment || i.attachmentList?.length ? 'å·²ä¸ä¼ éä»¶' : 'æªä¸ä¼ éä»¶' }} |
| | | </text> |
| | | </view> |
| | | <view class="primary-info"> |
| | | <text>{{ i.supplierName || '--' }}</text> |
| | |
| | | font-weight: 600; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | .attachment { |
| | | flex: none; |
| | | padding: 4rpx 10rpx; |
| | | color: #86909c; |
| | | background: #f2f3f5; |
| | | border-radius: 8rpx; |
| | | font-size: 20rpx; |
| | | } |
| | | .attachment.uploaded { |
| | | color: #16805b; |
| | | background: #eaf8f1; |
| | | } |
| | | .primary-info { |
| | | color: #4e5969; |
| | |
| | | ); |
| | | const itemProductPrice = (item: SaleOrderItem) => |
| | | Number(item.totalProductPrice ?? Number(item.productPrice || 0) * Number(item.count || 0)); |
| | | const attachmentName = (attachment: StorageFile) => |
| | | attachment.name || `éä»¶ #${attachment.id}`; |
| | | async function reload() { |
| | | if (state.accessToken && id.value) record.value = await getSaleOrder(state.accessToken, id.value); |
| | | } |
| | |
| | | } catch (e) { |
| | | uni.showToast({ title: e instanceof Error ? e.message : "æä½å¤±è´¥", icon: "none" }); |
| | | } |
| | | } |
| | | function preview(attachment: StorageFile) { |
| | | if (attachment.url) uni.previewImage({ urls: [attachment.url] }); |
| | | else uni.showToast({ title: "éä»¶å°åä¸å¯ç¨", icon: "none" }); |
| | | } |
| | | onLoad(async query => { |
| | | id.value = query.id ? Number(query.id) : undefined; |
| | |
| | | <text v-if="item.batchCode">åºåºæ¹å·ï¼{{ item.batchCode }}</text> |
| | | <text v-if="item.remark">{{ item.remark }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="card"> |
| | | <text class="cardTitle">éä»¶</text> |
| | | <view v-if="record.attachmentList?.length"> |
| | | <text v-for="attachment in record.attachmentList" |
| | | :key="attachment.id" |
| | | class="attachment" |
| | | @click="preview(attachment)">{{ attachmentName(attachment) }}</text> |
| | | </view> |
| | | <text v-else |
| | | class="empty">ææ éä»¶</text> |
| | | </view> |
| | | </template> |
| | | <view v-else |
| | |
| | | <text>é¢åæ¥æï¼{{item.validStartDate||'-'}}</text> |
| | | <text>å°ææ¥æï¼{{item.validEndDate||'-'}}</text> |
| | | <text>å建æ¶é´ï¼{{item.createTime||'-'}}</text> |
| | | <view class="attachmentList"><text class="attachmentLabel">éä»¶ï¼</text> |
| | | <view v-if="item.attachmentList?.length" |
| | | class="attachmentLinks"><text v-for="attachment in item.attachmentList" |
| | | :key="attachment.id" |
| | | @click="previewAttachment(attachment)">{{attachment.name||`éä»¶${attachment.id}`}}</text></view><text v-else>-</text> |
| | | </view> |
| | | <view class="actions"><button v-if="canUpdate" |
| | | @click="openEditor(item.id)">ç¼è¾</button><button v-if="canDelete" |
| | | class="danger" |