src/api/collaborativeApproval/approvalProcess.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,63 @@ // ååå®¡æ¹ import request from "@/utils/request"; export function approveProcessListPage(query) { return request({ url: '/approveProcess/list', method: 'get', params: query, }) } export function getDept(query) { return request({ url: '/approveProcess/getDept', method: 'get', params: query, }) } export function approveProcessGetInfo(query) { return request({ url: '/approveProcess/get', method: 'get', params: query, }) } // æ°å¢å®¡æ¹æµç¨ export function approveProcessAdd(query) { return request({ url: '/approveProcess/add', method: 'post', data: query, }) } // ä¿®æ¹å®¡æ¹æµç¨ export function approveProcessUpdate(query) { return request({ url: '/approveProcess/update', method: 'post', data: query, }) } // æäº¤å®¡æ¹ export function updateApproveNode(query) { return request({ url: '/approveNode/updateApproveNode', method: 'post', data: query, }) } // å é¤å®¡æ¹æµç¨ export function approveProcessDelete(query) { return request({ url: '/approveProcess/deleteIds', method: 'delete', data: query, }) } // æ¥è¯¢å®¡æ¹æµç¨ export function approveProcessDetails(query) { return request({ url: '/approveNode/details/' + query, method: 'get', }) } src/api/collaborativeApproval/noticeManagement.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,69 @@ import request from '@/utils/request' // æ¥è¯¢å ¬åå表 export function listNotice(query) { return request({ url: '/collaborativeApproval/notice/list', method: 'get', params: query }) } // æ¥è¯¢å ¬åè¯¦ç» export function getNotice(noticeId) { return request({ url: '/collaborativeApproval/notice/' + noticeId, method: 'get' }) } // æ°å¢å ¬å export function addNotice(data) { return request({ url: '/collaborativeApproval/notice', method: 'post', data: data }) } // ä¿®æ¹å ¬å export function updateNotice(data) { return request({ url: '/collaborativeApproval/notice', method: 'put', data: data }) } // å é¤å ¬å export function delNotice(noticeId) { return request({ url: '/collaborativeApproval/notice/' + noticeId, method: 'delete' }) } // æ¹éå é¤å ¬å export function delNoticeBatch(noticeIds) { return request({ url: '/collaborativeApproval/notice/batch', method: 'delete', data: noticeIds }) } // åå¸å ¬å export function publishNotice(noticeId) { return request({ url: '/collaborativeApproval/notice/publish/' + noticeId, method: 'put' }) } // ä¸çº¿å ¬å export function offlineNotice(noticeId) { return request({ url: '/collaborativeApproval/notice/offline/' + noticeId, method: 'put' }) } src/api/collaborativeApproval/rpaManagement.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,77 @@ import request from "@/utils/request"; // æ¥è¯¢RPAå表 export function listRpa(query) { return request({ url: "/collaborativeApproval/rpa/list", method: "get", params: query, }); } // æ¥è¯¢RPAè¯¦ç» export function getRpa(rpaId) { return request({ url: "/collaborativeApproval/rpa/" + rpaId, method: "get", }); } // æ°å¢RPA export function addRpa(data) { return request({ url: "/collaborativeApproval/rpa", method: "post", data: data, }); } // ä¿®æ¹RPA export function updateRpa(data) { return request({ url: "/collaborativeApproval/rpa", method: "put", data: data, }); } // å é¤RPA export function delRpa(rpaId) { return request({ url: "/collaborativeApproval/rpa/" + rpaId, method: "delete", }); } // æ¹éå é¤RPA export function delRpaBatch(rpaIds) { return request({ url: "/collaborativeApproval/rpa/batch", method: "delete", data: rpaIds, }); } // å¯å¨RPA export function startRpa(rpaId) { return request({ url: "/collaborativeApproval/rpa/start/" + rpaId, method: "post", }); } // 忢RPA export function stopRpa(rpaId) { return request({ url: "/collaborativeApproval/rpa/stop/" + rpaId, method: "post", }); } // è·åRPAç¶æ export function getRpaStatus(rpaId) { return request({ url: "/collaborativeApproval/rpa/status/" + rpaId, method: "get", }); } src/api/equipmentManagement/calibration.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,27 @@ // æ£å®æ ¡åè®°å½ import request from "@/utils/request"; // å页æ¥è¯¢ export function ledgerRecordListPage(query) { return request({ url: "/measuringInstrumentLedgerRecord/listPage", method: "get", params: query, }); } // æ ¡å export function ledgerRecordVerifying(query) { return request({ url: "/measuringInstrumentLedger/verifying", method: "post", data: query, }); } // ä¿®æ¹æ ¡å export function ledgerRecordUpdate(query) { return request({ url: "/measuringInstrumentLedgerRecord/update", method: "post", data: query, }); } src/api/equipmentManagement/ledger.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,44 @@ import request from "@/utils/request"; export const getLedgerPage = (params) => { return request({ url: "/device/ledger/page", method: "get", params, }); }; export const getLedgerById = (id) => { return request({ url: `/device/ledger/${id}`, method: "get", }); }; export const addLedger = (data) => { return request({ url: "/device/ledger", method: "post", data, }); }; export const editLedger = (data) => { return request({ url: "/device/ledger", method: "put", data, }); }; export const delLedger = (id) => { return request({ url: `/device/ledger/${id}`, method: "delete", }); }; export const getDeviceLedger = () => { return request({ url: "/device/ledger/getDeviceLedger", method: "get", }); }; src/api/equipmentManagement/measurementEquipment.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,35 @@ // 计éå¨å ·å°è´¦ import request from "@/utils/request"; // å页æ¥è¯¢ export function measuringInstrumentListPage(query) { return request({ url: "/measuringInstrumentLedger/listPage", method: "get", params: query, }); } // å é¤ export function measuringInstrumentDelete(query) { return request({ url: "/measuringInstrumentLedger/delete", method: "delete", data: query, }); } // æ°å¢ export function measuringInstrumentAdd(query) { return request({ url: "/measuringInstrumentLedger/add", method: "post", data: query, }); } // ä¿®æ¹ export function measuringInstrumentUpdate(query) { return request({ url: "/measuringInstrumentLedger/update", method: "post", data: query, }); } src/api/equipmentManagement/repair.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,72 @@ import request from "@/utils/request"; /** * @desc è®¾å¤æ¥ä¿®å表 * @param {å页æ¥è¯¢} params * @returns */ export const getRepairPage = (params) => { return request({ url: "/device/repair/page", method: "get", params, }); }; /** * @desc æ°å¢æ¥ä¿® * @param {æ¥ä¿®åæ°} data * @returns */ export const addRepair = (data) => { return request({ url: "/device/repair", method: "post", data, }); }; /** * @desc ç¼è¾æ¥ä¿® * @param {æ¥ä¿®åæ°} data * @returns */ export const editRepair = (data) => { return request({ url: "/device/repair", method: "put", data, }); }; /** * @desc æ ¹æ®idæ¥è¯¢ä¸æ¡æ¥ä¿® * @param {æ¥ä¿®id} id * @returns */ export const getRepairById = (id) => { return request({ url: `/device/repair/${id}`, method: "get", }); }; /** * @desc å 餿¥ä¿® * @param {ç¼å·} ids * @returns */ export const delRepair = (ids) => { return request({ url: `/device/repair/${ids}`, method: "delete", }); }; export const addMaintain = (data) => { return request({ url: `/device/repair/repair`, method: "post", data, }); }; src/api/equipmentManagement/upkeep.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,72 @@ import request from "@/utils/request"; /** * @desc 设å¤ä¿å »å表å页æ¥è¯¢ * @param {å页æ¥è¯¢å ¥å} params * @returns */ export const getUpkeepPage = (params) => { return request({ url: "/device/maintenance/page", method: "get", params, }); }; /** * @desc 设å¤ä¿å »è¯¦æ * @param {ä¿å »ä½ç¼å·} id * @returns */ export const getUpkeepById = (id) => { return request({ url: `/device/maintenance/${id}`, method: "get", }); }; /** * @desc 设å¤ä¿å »æ°å¢ * @param {æ°å¢ä¿å »è¡¨å} data * @returns */ export const addUpkeep = (data) => { return request({ url: "/device/maintenance", method: "post", data, }); }; /** * @desc 设å¤ä¿å »ç¼è¾ * @param {ç¼è¾ä¿å »è¡¨å} data * @returns */ export const editUpkeep = (data) => { return request({ url: "/device/maintenance", method: "put", data, }); }; /** * @desc æ°å¢ä¿å »è¡¨å * @param {æ°å¢ä¿å »è¡¨å} data * @returns */ export const addMaintenance = (data) => { return request({ url: "/device/maintenance/maintenance", method: "post", data, }); }; export const delUpkeep = (id) => { return request({ url: `/device/maintenance/${id}`, method: "delete", }); }; src/api/system/user.js
@@ -45,4 +45,11 @@ url: '/system/user/userListNoPage', method: 'get' }) } // æ¥è¯¢ç¨æ·å表 export function userListNoPageByTenantId() { return request({ url: '/system/user/userListNoPageByTenantId', method: 'get' }) } src/pages.json
@@ -273,6 +273,20 @@ } }, { "path": "pages/cooperativeOffice/collaborativeApproval/approve", "style": { "navigationBarTitleText": "å®¡æ ¸", "navigationStyle": "custom" } }, { "path": "pages/cooperativeOffice/collaborativeApproval/contactSelect", "style": { "navigationBarTitleText": "éæ©è系人", "navigationStyle": "custom" } }, { "path": "pages/cooperativeOffice/clientVisit/index", "style": { "navigationBarTitleText": "å®¢æ·æè®¿", src/pages/cooperativeOffice/collaborativeApproval/approve.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,519 @@ <template> <view class="approve-page"> <PageHeader title="å®¡æ ¸" @back="goBack" /> <!-- ç³è¯·ä¿¡æ¯ --> <view class="application-info"> <view class="info-header"> <text class="info-title">ç³è¯·ä¿¡æ¯</text> </view> <view class="info-content"> <view class="info-row"> <text class="info-label">ç³è¯·äºº</text> <text class="info-value">{{ approvalData.approveUserName }}</text> </view> <view class="info-row"> <text class="info-label">ç³è¯·é¨é¨</text> <text class="info-value">{{ approvalData.approveDeptName }}</text> </view> <view class="info-row"> <text class="info-label">ç³è¯·äºç±</text> <text class="info-value">{{ approvalData.approveReason }}</text> </view> <view class="info-row"> <text class="info-label">ç³è¯·æ¥æ</text> <text class="info-value">{{ approvalData.approveTime }}</text> </view> </view> </view> <!-- å®¡æ¹æµç¨ --> <view class="approval-process"> <view class="process-header"> <text class="process-title">å®¡æ¹æµç¨</text> </view> <view class="process-steps"> <view v-for="(step, index) in approvalSteps" :key="index" class="process-step" :class="{ 'completed': step.status === 'completed', 'current': step.status === 'current', 'pending': step.status === 'pending', 'rejected': step.status === 'rejected' }" > <view class="step-indicator"> <view class="step-dot"> <text v-if="step.status === 'completed'" class="step-icon">â</text> <text v-else-if="step.status === 'rejected'" class="step-icon">â</text> <text v-else class="step-number">{{ index + 1 }}</text> </view> <view v-if="index < approvalSteps.length - 1" class="step-line"></view> </view> <view class="step-content"> <view class="step-info"> <text class="step-title">{{ step.title }}</text> <text class="step-approver">{{ step.approverName }}</text> <text v-if="step.approveTime" class="step-time">{{ step.approveTime }}</text> </view> <view v-if="step.opinion" class="step-opinion"> <text class="opinion-label">å®¡æ¹æè§ï¼</text> <text class="opinion-content">{{ step.opinion }}</text> </view> <!-- ç¾åå±ç¤º --> <view v-if="step.urlTem" class="step-opinion" style="margin-top:8px;"> <text class="opinion-label">ç¾åï¼</text> <image :src="step.urlTem" mode="widthFix" style="width:180px;border-radius:6px;border:1px solid #eee;" /> </view> </view> </view> </view> </view> <!-- å®¡æ ¸æè§è¾å ¥ --> <view v-if="canApprove" class="approval-input"> <view class="input-header"> <text class="input-title">å®¡æ ¸æè§</text> </view> <view class="input-content"> <van-field v-model="approvalOpinion" type="textarea" rows="4" placeholder="请è¾å ¥å®¡æ ¸æè§" maxlength="200" show-word-limit /> </view> </view> <!-- åºé¨æä½æé® --> <view v-if="canApprove" class="footer-actions"> <van-button class="reject-btn" @click="handleReject">驳å</van-button> <van-button class="approve-btn" @click="handleApprove">éè¿</van-button> </view> </view> </template> <script setup> import { ref, onMounted, computed } from 'vue' import { approveProcessGetInfo, approveProcessDetails, updateApproveNode } from '@/api/collaborativeApproval/approvalProcess' import useUserStore from '@/store/modules/user' import { showToast } from 'vant' import PageHeader from "@/components/PageHeader.vue"; const userStore = useUserStore() const approvalData = ref({}) const approvalSteps = ref([]) const approvalOpinion = ref('') const approveId = ref('') // ä»è¯¦æ æ¥å£åæ®µå¯¹é½ canApproveï¼ä» 彿 isShen çèç¹æ¶å¯å®¡æ¹ const canApprove = computed(() => { return approvalSteps.value.some(step => step.isShen === true) }) onMounted(() => { const pages = getCurrentPages() const currentPage = pages[pages.length - 1] approveId.value = currentPage.options.approveId if (approveId.value) { loadApprovalData() } }) const loadApprovalData = () => { // åºæ¬ç³è¯·ä¿¡æ¯ approveProcessGetInfo({ id: approveId.value }).then(res => { approvalData.value = res.data || {} }) // 审æ¹èç¹è¯¦æ approveProcessDetails(approveId.value).then(res => { const list = Array.isArray(res.data) ? res.data : [] // ä¿ååå§èç¹æ°æ®ä¾æäº¤ä½¿ç¨ activities.value = list approvalSteps.value = list.map((it, idx) => { // èç¹ç¶ææ å°ï¼1=éè¿ï¼2=ä¸éè¿ï¼å¦åçæ¯å¦å½å(isShen)ï¼åé»è®¤ä¸ºå¾ å¤ç let status = 'pending' if (it.approveNodeStatus === 1) status = 'completed' else if (it.approveNodeStatus === 2) status = 'rejected' else if (it.isShen) status = 'current' return { title: `第${idx + 1}æ¥å®¡æ¹`, approverName: it.approveNodeUser || 'æªç¥ç¨æ·', status, approveTime: it.approveTime || null, opinion: it.approveNodeReason || '', urlTem: it.urlTem || '', isShen: !!it.isShen } }) }) } const goBack = () => { uni.navigateBack() } const submitForm = (status) => { // å¯éï¼æ ¡éªå®¡æ ¸æè§ if (!approvalOpinion.value?.trim()) { showToast('请è¾å ¥å®¡æ ¸æè§') return } // æ¾å°å½åå¯å®¡æ¹èç¹ const filteredActivities = activities.value.filter(activity => activity.isShen) if (!filteredActivities.length) { showToast('å½åæ å¯å®¡æ¹èç¹') return } // åå ¥ç¶æåæè§ filteredActivities[0].approveNodeStatus = status filteredActivities[0].approveNodeReason = approvalOpinion.value || '' // è®¡ç®æ¯å¦ä¸ºæå䏿¥ const isLast = activities.value.findIndex(a => a.isShen) === activities.value.length - 1 // è°ç¨å端 updateApproveNode({ ...filteredActivities[0], isLast }).then(() => { const msg = status === 1 ? '审æ¹éè¿' : '审æ¹å·²é©³å' showToast(msg) // æç¤ºåè¿åä¸ä¸ä¸ªé¡µé¢ setTimeout(() => { goBack() // å 鍿¯ uni.navigateBack() }, 800) }) } const handleApprove = () => { uni.showModal({ title: '确认æä½', content: 'ç¡®å®è¦éè¿æ¤å®¡æ¹åï¼', success: (res) => { if (res.confirm) submitForm(1) } }) } const handleReject = () => { uni.showModal({ title: '确认æä½', content: 'ç¡®å®è¦é©³åæ¤å®¡æ¹åï¼', success: (res) => { if (res.confirm) submitForm(2) } }) } // åå§èç¹æ°æ®ï¼ç¨äºæäº¤é»è¾ï¼ const activities = ref([]) </script> <style scoped lang="scss"> .approve-page { min-height: 100vh; background: #f8f9fa; padding-bottom: 80px; } .header { display: flex; align-items: center; background: #fff; padding: 16px 20px; border-bottom: 1px solid #f0f0f0; position: sticky; top: 0; z-index: 100; } .title { flex: 1; text-align: center; font-size: 18px; font-weight: 600; color: #333; } .application-info { background: #fff; margin: 16px; border-radius: 12px; overflow: hidden; } .info-header { padding: 16px; border-bottom: 1px solid #f0f0f0; background: #f8f9fa; } .info-title { font-size: 16px; font-weight: 600; color: #333; } .info-content { padding: 16px; } .info-row { display: flex; align-items: center; margin-bottom: 12px; &:last-child { margin-bottom: 0; } } .info-label { font-size: 14px; color: #666; width: 80px; flex-shrink: 0; } .info-value { font-size: 14px; color: #333; flex: 1; } .approval-process { background: #fff; margin: 16px; border-radius: 12px; overflow: hidden; } .process-header { padding: 16px; border-bottom: 1px solid #f0f0f0; background: #f8f9fa; } .process-title { font-size: 16px; font-weight: 600; color: #333; } .process-steps { padding: 20px; } .process-step { display: flex; position: relative; margin-bottom: 24px; &:last-child { margin-bottom: 0; .step-line { display: none; } } } .step-indicator { display: flex; flex-direction: column; align-items: center; margin-right: 16px; } .step-dot { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 600; position: relative; z-index: 2; } .process-step.completed .step-dot { background: #52c41a; color: #fff; } .process-step.current .step-dot { background: #1890ff; color: #fff; animation: pulse 2s infinite; } .process-step.pending .step-dot { background: #d9d9d9; color: #999; } .step-line { width: 2px; height: 40px; background: #d9d9d9; margin-top: 8px; } .process-step.completed .step-line { background: #52c41a; } .process-step.rejected .step-dot { background: #ff4d4f; color: #fff; } .process-step.rejected .step-line { background: #ff4d4f; } .step-content { flex: 1; padding-top: 4px; } .step-info { margin-bottom: 8px; } .step-title { font-size: 16px; font-weight: 600; color: #333; display: block; margin-bottom: 4px; } .step-approver { font-size: 14px; color: #666; display: block; margin-bottom: 4px; } .step-time { font-size: 12px; color: #999; display: block; } .step-opinion { background: #f8f9fa; padding: 12px; border-radius: 8px; border-left: 4px solid #52c41a; } .opinion-label { font-size: 12px; color: #666; display: block; margin-bottom: 4px; } .opinion-content { font-size: 14px; color: #333; line-height: 1.5; } .approval-input { background: #fff; margin: 16px; border-radius: 12px; overflow: hidden; } .input-header { padding: 16px; border-bottom: 1px solid #f0f0f0; background: #f8f9fa; } .input-title { font-size: 16px; font-weight: 600; color: #333; } .input-content { padding: 16px; } .footer-actions { position: fixed; left: 0; right: 0; bottom: 0; background: #fff; display: flex; justify-content: space-around; align-items: center; padding: 16px; box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1); z-index: 1000; } .reject-btn { width: 120px; background: #ff4d4f; color: #fff; border: none; } .approve-btn { width: 120px; background: #52c41a; color: #fff; border: none; } @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(24, 144, 255, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(24, 144, 255, 0); } 100% { box-shadow: 0 0 0 0 rgba(24, 144, 255, 0); } } .signature-section { background: #fff; padding: 12px 16px 16px; border-top: 1px solid #f0f0f0; } .signature-header { margin-bottom: 8px; } .signature-title { font-size: 14px; font-weight: 600; color: #333; } .signature-box { width: 100%; height: 180px; background: #fff; border: 1px dashed #d9d9d9; border-radius: 8px; overflow: hidden; } .signature-actions { margin-top: 8px; display: flex; justify-content: flex-end; } </style> src/pages/cooperativeOffice/collaborativeApproval/contactSelect.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,391 @@ <template> <view class="contact-select"> <!-- 顶鍿 颿 --> <view class="header"> <up-icon name="arrow-left" size="20" color="#333" @click="goBack" /> <text class="title">éæ©è系人</text> <text class="confirm-btn" @click="confirmSelect">ç¡®å®</text> </view> <!-- æç´¢æ¡ --> <!-- <view class="search-section">--> <!-- <van-search--> <!-- v-model="searchValue"--> <!-- placeholder="æç´¢è系人"--> <!-- @search="onSearch"--> <!-- @input="onSearch"--> <!-- />--> <!-- </view>--> <!-- 已鿩çè系人 --> <view class="selected-section" v-if="selectedContact"> <view class="selected-header"> <text class="selected-title">已鿩</text> <text class="clear-btn" @click="clearSelected">æ¸ ç©º</text> </view> <view class="selected-item"> <view class="contact-avatar"> <text class="avatar-text">{{ selectedContact.nickName.charAt(0) }}</text> </view> <view class="contact-details"> <text class="contact-name">{{ selectedContact.nickName }}</text> </view> <van-icon name="cross" size="16" color="#999" @click="clearSelected" /> </view> </view> <!-- è系人å表 --> <view class="contact-list"> <view class="list-header"> <text class="list-title">å ¨é¨è系人</text> </view> <van-list v-model:loading="loading" :finished="finished" finished-text="æ²¡ææ´å¤äº" @load="onLoad" > <view v-for="contact in userList" :key="contact.userId" class="contact-item" :class="{ 'selected': isSelected(contact) }" @click="selectContact(contact)" > <view class="contact-info"> <view class="contact-avatar"> <text class="avatar-text">{{ contact.nickName.charAt(0) }}</text> </view> <view class="contact-details"> <text class="contact-name">{{ contact.nickName }}</text> <!-- <text class="contact-dept">{{ contact.department }}</text>--> </view> </view> </view> </van-list> </view> </view> </template> <script setup> import { ref, onMounted } from 'vue' import { userListNoPageByTenantId } from "@/api/system/user" const loading = ref(false) const finished = ref(false) const selectedContact = ref(null) const userList = ref([]) // æ¥æ¶ä¼ éçåæ° const stepIndex = ref(0) onMounted(() => { // è·å页é¢åæ° const pages = getCurrentPages() const currentPage = pages[pages.length - 1] if (currentPage.options.stepIndex !== undefined) { stepIndex.value = parseInt(currentPage.options.stepIndex) } // åå§åèç³»äººæ°æ® initContacts() }) const initContacts = () => { userListNoPageByTenantId().then((res) => { userList.value = res.data }) finished.value = true } const onLoad = () => { // 模æå è½½æ´å¤æ°æ® setTimeout(() => { loading.value = false finished.value = true }, 1000) } const isSelected = (contact) => { return selectedContact.value && selectedContact.value.userId === contact.userId } const selectContact = (contact) => { // å鿍¡å¼ï¼ç´æ¥æ¿æ¢éä¸çè系人 selectedContact.value = contact } const clearSelected = () => { selectedContact.value = null } const goBack = () => { uni.navigateBack() } const confirmSelect = () => { if (!selectedContact.value) { uni.showToast({ title: 'è¯·éæ©ä¸ä¸ªè系人', icon: 'none' }) return } // ä½¿ç¨ uni.$emit åéæ°æ® uni.$emit('selectContact', { stepIndex: stepIndex.value, contact: selectedContact.value }) uni.navigateBack() } </script> <style scoped lang="scss"> .contact-select { min-height: 100vh; background: #f8f9fa; } .header { background: #ffffff; padding: 16px 20px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #f0f0f0; position: sticky; /* å ¼å®¹ iOS åæµ·/çµå¨å²å®å ¨åº */ padding-top: calc(env(safe-area-inset-top)); top: 0; z-index: 100; position: relative; } .title { font-size: 18px; font-weight: 600; color: #333; } .confirm-btn { color: #006cfb; font-size: 16px; font-weight: 500; } .search-section { background: #fff; padding: 12px 16px; border-bottom: 1px solid #f0f0f0; } .selected-section { background: #fff; margin-top: 8px; padding: 16px; } .selected-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; } .selected-title { font-size: 14px; color: #333; font-weight: 500; } .clear-btn { color: #006cfb; font-size: 14px; } .selected-item { display: flex; align-items: center; background: #f0f8ff; border: 1px solid #006cfb; border-radius: 12px; padding: 12px; gap: 12px; position: relative; &::before { content: ''; position: absolute; top: -2px; right: -2px; width: 16px; height: 16px; background: #52c41a; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } &::after { content: 'â'; position: absolute; top: -1px; right: -1px; width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #fff; font-weight: bold; } } .contact-list { background: #fff; margin-top: 8px; } .list-header { padding: 16px; border-bottom: 1px solid #f0f0f0; } .list-title { font-size: 14px; color: #666; } .contact-item { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid #f8f9fa; transition: all 0.2s; position: relative; &.selected { background-color: #f0f8ff; &::before { content: ''; position: absolute; left: 8px; top: 50%; transform: translateY(-50%); width: 4px; height: 4px; background: #006cfb; border-radius: 50%; box-shadow: 0 0 0 4px rgba(0, 108, 251, 0.2); } } &:active { background-color: #f5f5f5; } } .contact-info { display: flex; align-items: center; flex: 1; padding-left: 16px; } .contact-avatar { width: 40px; height: 40px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-right: 12px; position: relative; } .avatar-text { color: #fff; font-size: 16px; font-weight: 500; } .contact-details { flex: 1; } .contact-name { display: block; font-size: 16px; color: #333; } .contact-dept { font-size: 12px; color: #999; } // èªå®ä¹åéæé®æ ·å¼ :deep(.van-radio) { .van-radio__icon { width: 20px; height: 20px; border: 2px solid #ddd; border-radius: 50%; background: #fff; position: relative; transition: all 0.2s; &::before { content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0); width: 8px; height: 8px; background: #006cfb; border-radius: 50%; transition: transform 0.2s; } } &.van-radio--checked { .van-radio__icon { border-color: #006cfb; background: #fff; &::before { transform: translate(-50%, -50%) scale(1); } &::after { content: ''; position: absolute; top: -2px; left: -2px; right: -2px; bottom: -2px; border: 2px solid rgba(0, 108, 251, 0.2); border-radius: 50%; animation: ripple 0.6s ease-out; } } } } @keyframes ripple { 0% { transform: scale(0.8); opacity: 1; } 100% { transform: scale(1.2); opacity: 0; } } </style> src/pages/cooperativeOffice/collaborativeApproval/detail.vue
@@ -1,58 +1,74 @@ <template> <view class="account-detail"> <!-- 顶鍿 颿 --> <view class="header"> <up-icon name="arrow-left" size="20" color="#333" @click="goBack" /> <text class="title">å®¡æ¹æµç¨</text> </view> <PageHeader title="å®¡æ¹æµç¨" @back="goBack" /> <!-- 表ååºå --> <view class="form-section"> <van-form ref="formRef" @submit="submitForm" :rules="rules" input-align="right"> <van-cell-group inset style="height:auto"> <van-form ref="formRef" @submit="submitForm" :rules="rules" input-align="right" error-message-align="right" scroll-to-error scroll-to-error-position="center"> <van-cell-group style="margin-bottom: 16px;"> <van-field v-model="form.approveReason" name="approveReason" rows="2" autosize label="ç³è¯·äºç±" type="textarea" maxlength="200" :rules="[{ required: true, message: 'ç³è¯·äºç±ä¸è½ä¸ºç©º' }]" placeholder="请è¾å ¥ç³è¯·äºç±" show-word-limit required /> </van-cell-group> <van-cell-group> <van-field v-model="form.approveDeptName" readonly name="picker" label="ç³è¯·é¨é¨" placeholder="è¯·éæ©ç³è¯·é¨é¨" :rules="[{ required: true, message: 'è¯·éæ©ç³è¯·é¨é¨' }]" @click="showPicker = true" required /> <van-field v-model="taxPrice" v-model="form.approveUserName" name="taxPrice" label="å§å" placeholder="请è¾å ¥å§å" :rules="[{ required: true, message: 'å§åä¸è½ä¸ºç©º' }]" label="ç³è¯·äºº" placeholder="请è¾å ¥ç³è¯·äºº" :rules="[{ required: true, message: 'ç³è¯·äººä¸è½ä¸ºç©º' }]" required readonly /> <van-field v-model="result" readonly name="picker" label="ç³è¯·é¨é¨" placeholder="è¯·éæ©ç³è¯·é¨é¨" :rules="[{ required: true, message: 'è¯·éæ©ç³è¯·é¨é¨' }]" @click="showPicker = true" required /> <van-popup v-model:show="showPicker" destroy-on-close position="bottom" > <van-picker :columns="columns" :columns="productOptions" :model-value="pickerValue" @confirm="onConfirm" @cancel="showPicker = false" /> </van-popup> <van-field v-model="message" name="message" rows="1" autosize label="ç³è¯·äºç±" type="textarea" placeholder="请è¾å ¥ç³è¯·äºç±" height="100" :rules="[{ required: true, message: 'ç³è¯·äºç±ä¸è½ä¸ºç©º' }]" required /> <van-field v-model="form.approveTime" label="ç³è¯·æ¥æ" placeholder="è¯·éæ©" readonly required @click="showDatePicker" :rules="[{ required: true, message: 'è¯·éæ©æ¥æ¬¾æ¥æ' }]" /> <!-- æ¥æéæ©å¨ --> <van-popup v-model:show="showDate" position="bottom"> <van-date-picker v-model="currentDate" title="éæ©æ¥æ" @confirm="onDateConfirm" @cancel="showDate = false" /> </van-popup> </van-cell-group> </van-form> </view> @@ -60,30 +76,38 @@ <view class="approval-process"> <view class="approval-header"> <text class="approval-title">å®¡æ ¸æµç¨</text> <text class="approval-desc">å·²ç±ç®¡çåé¢è®¾ä¸å¯ä¿®æ¹</text> <text class="approval-desc">æ¯ä¸ªæ¥éª¤åªè½éæ©ä¸ä¸ªå®¡æ¹äºº</text> </view> <view class="approval-steps"> <view v-for="(step, stepIndex) in approvalSteps" :key="stepIndex" class="approval-step"> <view v-for="(step, stepIndex) in approverNodes" :key="stepIndex" class="approval-step"> <view class="step-dot"></view> <view class="step-title"> <text>审æ¹äºº</text> </view> <view class="approvers-container"> <view v-for="(approver, approverIndex) in step.approvers" :key="approverIndex" class="approver-item"> <view class="approver-avatar"></view> <text class="approver-name">{{ approver.name }}</text> <view class="delete-approver-btn" @click="removeApprover(stepIndex, approverIndex)">Ã</view> <view class="approver-container"> <view v-if="step.nickName" class="approver-item"> <view class="approver-avatar"> <text class="avatar-text">{{ step.nickName.charAt(0) }}</text> <view class="status-dot"></view> </view> <view class="approver-info"> <text class="approver-name">{{ step.nickName }}</text> </view> <view class="delete-approver-btn" @click="removeApprover(stepIndex)">Ã</view> </view> <view class="add-approver-btn" @click="addApprover(stepIndex)">+ <view v-else class="add-approver-btn" @click="addApprover(stepIndex)"> <view class="add-circle">+</view> <text class="add-label">鿩审æ¹äºº</text> </view> </view> <view class="step-line" v-if="stepIndex < approvalSteps.length - 1"></view> <view class="delete-step-btn" @click="removeApprovalStep(stepIndex)">å é¤èç¹</view> <view class="step-line" v-if="stepIndex < approverNodes.length - 1"></view> <view class="delete-step-btn" v-if="approverNodes.length > 1" @click="removeApprovalStep(stepIndex)">å é¤èç¹</view> </view> </view> <view class="add-step-btn" @click="addApprovalStep"> <text>æ°å¢èç¹å®¡æ ¸äºº</text> <view class="add-step-btn"> <van-button icon="plus" plain type="primary" style="width: 100%" @click="addApprovalStep">æ°å¢èç¹</van-button> </view> </view> @@ -95,166 +119,238 @@ </view> </template> <script> import { ref, onMounted } from "vue"; <script setup> import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue"; import PageHeader from "@/components/PageHeader.vue"; import useUserStore from "@/store/modules/user"; import {getDept, approveProcessGetInfo, approveProcessAdd, approveProcessUpdate} from "@/api/collaborativeApproval/approvalProcess"; import { showToast } from 'vant' import {userListNoPageByTenantId} from "@/api/system/user"; export default { setup() { const rules = ref({ taxPrice: { rules: [{ required: true, errorMessage: 'å§åä¸è½ä¸ºç©º' }] }, result: { rules: [{ required: true, errorMessage: 'è¯·éæ©ç³è¯·é¨é¨' }] }, message: { rules: [{ required: true, errorMessage: 'ç³è¯·äºç±ä¸è½ä¸ºç©º' }] }, const data = reactive({ form: { approveTime: "", approveId: "", approveUser: "", approveUserName: "", approveDeptName: "", approveDeptId: "", approveReason: "", checkResult: "", tempFileIds: [], approverList: [] // æ°å¢å段ï¼å卿æèç¹ç审æ¹äººid }, rules: { approveTime: [{ required: false, message: "请è¾å ¥", trigger: "change" },], approveId: [{ required: false, message: "请è¾å ¥", trigger: "blur" }], approveUser: [{ required: false, message: "请è¾å ¥", trigger: "blur" }], approveDeptId: [{ required: true, message: "请è¾å ¥", trigger: "blur" }], approveReason: [{ required: true, message: "请è¾å ¥", trigger: "blur" }], checkResult: [{ required: false, message: "请è¾å ¥", trigger: "blur" }], }, }); const result = ref(""); const pickerValue = ref([]); const showPicker = ref(false); const columns = ref([]); onMounted(async () => { try { // æ¿æ¢ä¸ºå®é æ¥å£å°å // const response = await axios.get('/api/getDepartments'); columns.value = [ { text: "æå·", value: "Hangzhou", }, { text: "宿³¢", value: "Ningbo", }, { text: "温å·", value: "Wenzhou", }, { text: "ç»å ´", value: "Shaoxing", }, { text: "æ¹å·", value: "Huzhou", }, ]; } catch (error) { console.error("è·åé¨é¨æ°æ®å¤±è´¥:", error); } }); const onConfirm = ({ selectedValues, selectedOptions }) => { result.value = selectedOptions[0]?.text; pickerValue.value = selectedValues; showPicker.value = false; }; const taxPrice = ref(""); const contractAmount = ref(""); const approvalSteps = ref([ { approvers: [{ name: 'å¢å°æ' }, { name: 'å¢å°æ' }] }, { approvers: [{ name: 'å¢å°æ' }] }, { approvers: [{ name: 'å¢å°æ' }] }, { approvers: [{ name: 'å¢å°æ' }] } ]); const { form, rules } = toRefs(data); const result = ref(""); const pickerValue = ref([]); const showPicker = ref(false); const productOptions = ref([]); const operationType = ref(""); const currentApproveStatus = ref(""); const approverNodes = ref([]); const userList = ref([]); const formRef = ref(null); const message = ref(""); const showDate = ref(false) const currentDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]) const userStore = useUserStore() const goBack = () => { uni.navigateBack(); }; const formRef = ref(null); const submitForm = () => { formRef.value.validate().then(() => { // è¡¨åæ ¡éªéè¿ï¼å¯ä»¥æäº¤æ°æ® console.log("è¡¨åæ°æ®:", { taxPrice: taxPrice.value, department: result.value, message: message.value, approvalSteps: approvalSteps.value }); uni.showToast({ title: "ä¿åæå", icon: "success", }); }).catch((error) => { console.error("è¡¨åæ ¡éªå¤±è´¥:", error); // æ¾ç¤ºå ·ä½çéè¯¯ä¿¡æ¯ if (error.length > 0) { const firstError = error[0]; uni.showToast({ title: firstError.message || 'è¡¨åæ ¡éªå¤±è´¥', icon: 'none' }); } else { uni.showToast({ title: 'è¡¨åæ ¡éªå¤±è´¥ï¼è¯·æ£æ¥å¿ 填项', icon: 'none' }); } }); }; const message = ref(""); const addApprover = (stepIndex) => { // 卿å®å®¡æ¹æ¥éª¤æ·»å æ°ç审æ¹äºº approvalSteps.value[stepIndex].approvers.push({ name: 'å¢å°æ' }); }; const addApprovalStep = () => { // æ·»å æ°çå®¡æ¹æ¥éª¤ approvalSteps.value.push({ approvers: [{ name: 'å¢å°æ' }] }); }; const removeApprover = (stepIndex, approverIndex) => { // ç¡®ä¿æ¯ä¸ªæ¥éª¤è³å°ä¿çä¸ä¸ªå®¡æ¹äºº if (approvalSteps.value[stepIndex].approvers.length > 1) { approvalSteps.value[stepIndex].approvers.splice(approverIndex, 1); } else { uni.showToast({ title: 'æ¯ä¸ªæ¥éª¤è³å°éè¦ä¸ä¸ªå®¡æ¹äºº', icon: 'none' }); } }; const removeApprovalStep = (stepIndex) => { // ç¡®ä¿è³å°ä¿çä¸ä¸ªå®¡æ¹æ¥éª¤ if (approvalSteps.value.length > 1) { approvalSteps.value.splice(stepIndex, 1); } else { uni.showToast({ title: 'è³å°éè¦ä¸ä¸ªå®¡æ¹æ¥éª¤', icon: 'none' }); } }; return { rules, removeApprovalStep, removeApprover, result, pickerValue, columns, onConfirm, showPicker, taxPrice, contractAmount, goBack, submitForm, approvalSteps, addApprover, addApprovalStep, formRef, message }; }, const getProductOptions = () => { getDept().then((res) => { productOptions.value = res.data.map(item => ({ value: item.deptId, text: item.deptName })) }); }; const fileList = ref([]); let nextApproverId = 2; onMounted(async () => { try { getProductOptions() userListNoPageByTenantId().then((res) => { userList.value = res.data }) form.value.approveUser = userStore.id form.value.approveUserName = userStore.nickName form.value.approveTime = getCurrentDate(); // è·åURLåæ° const pages = getCurrentPages(); const currentPage = pages[pages.length - 1]; operationType.value = currentPage.options.operationType || 'add'; // 妿æ¯ç¼è¾æ¨¡å¼ï¼ä»æ¬å°åå¨è·åæ°æ® if (operationType.value === 'edit') { const storedData = uni.getStorageSync('invoiceLedgerEditRow'); if (storedData) { const row = JSON.parse(storedData); fileList.value = row.commonFileList || []; form.value.tempFileIds = fileList.value.map(file => file.id); currentApproveStatus.value = row.approveStatus; approveProcessGetInfo({id: row.approveId, approveReason: '1'}).then(res => { form.value = {...res.data}; // 忾审æ¹äºº if (res.data && res.data.approveUserIds) { const userIds = res.data.approveUserIds.split(','); approverNodes.value = userIds.map((userId, idx) => { const userIdNum = parseInt(userId.trim()); // ä»userList䏿¾å°å¯¹åºçç¨æ·ä¿¡æ¯ const userInfo = userList.value.find(user => user.userId === userIdNum); return { id: idx + 1, userId: userIdNum, nickName: userInfo ? userInfo.nickName : null }; }); nextApproverId = userIds.length + 1; } else { // æ°å¢æ¨¡å¼ï¼åå§åä¸ä¸ªç©ºç审æ¹èç¹ approverNodes.value = [{ id: 1, userId: null, nickName: null }]; nextApproverId = 2; } }); } } else { // æ°å¢æ¨¡å¼ï¼åå§åä¸ä¸ªç©ºç审æ¹èç¹ approverNodes.value = [{ id: 1, userId: null }]; } // çå¬èç³»äººéæ©äºä»¶ uni.$on('selectContact', handleSelectContact); } catch (error) { console.error("è·åé¨é¨æ°æ®å¤±è´¥:", error); } }); onUnmounted(() => { // ç§»é¤äºä»¶çå¬ uni.$off('selectContact', handleSelectContact); }); const onConfirm = ({ selectedValues, selectedOptions }) => { form.value.approveDeptName = selectedOptions[0]?.text; form.value.approveDeptId = selectedOptions[0]?.value; pickerValue.value = selectedValues; showPicker.value = false; }; const goBack = () => { // æ¸ é¤æ¬å°åå¨çæ°æ® uni.removeStorageSync('invoiceLedgerEditRow'); uni.navigateBack(); }; const submitForm = () => { // æ£æ¥æ¯ä¸ªå®¡æ¹æ¥éª¤æ¯å¦é½æå®¡æ¹äºº const hasEmptyStep = approverNodes.value.some(step => !step.nickName); if (hasEmptyStep) { showToast('请为æ¯ä¸ªå®¡æ¹æ¥éª¤éæ©å®¡æ¹äºº'); return; } formRef.value.validate().then(() => { // è¡¨åæ ¡éªéè¿ï¼å¯ä»¥æäº¤æ°æ® // æ¶éææèç¹ç审æ¹äººid console.log('approverNodes---', approverNodes.value) form.value.approveUserIds = approverNodes.value.map(node => node.userId).join(',') form.value.approveType = 0 if (operationType.value === "add" || currentApproveStatus.value == 3) { approveProcessAdd(form.value).then(res => { showToast("æäº¤æå"); goBack() }) } else { approveProcessUpdate(form.value).then(res => { showToast("æäº¤æå"); goBack() }) } }).catch((error) => { console.error("è¡¨åæ ¡éªå¤±è´¥:", error); // æ¾ç¤ºå ·ä½çéè¯¯ä¿¡æ¯ if (error.length > 0) { const firstError = error[0]; uni.showToast({ title: firstError.message || 'è¡¨åæ ¡éªå¤±è´¥', icon: 'none' }); } else { uni.showToast({ title: 'è¡¨åæ ¡éªå¤±è´¥ï¼è¯·æ£æ¥å¿ 填项', icon: 'none' }); } }); }; // å¤çèç³»äººéæ©ç»æ const handleSelectContact = (data) => { const { stepIndex, contact } = data; // å°éä¸çè系人设置为对åºå®¡æ¹æ¥éª¤ç审æ¹äºº approverNodes.value[stepIndex].userId = contact.userId; approverNodes.value[stepIndex].nickName = contact.nickName; }; const addApprover = (stepIndex) => { // 跳转å°èç³»äººéæ©é¡µé¢ uni.navigateTo({ url: `/pages/cooperativeOffice/collaborativeApproval/contactSelect?stepIndex=${stepIndex}` }); }; const addApprovalStep = () => { // æ·»å æ°çå®¡æ¹æ¥éª¤ approverNodes.value.push({ userId: null, nickName: null }); }; const removeApprover = (stepIndex) => { // ç§»é¤å®¡æ¹äºº approverNodes.value[stepIndex].userId = null; approverNodes.value[stepIndex].nickName = null; }; const removeApprovalStep = (stepIndex) => { // ç¡®ä¿è³å°ä¿çä¸ä¸ªå®¡æ¹æ¥éª¤ if (approverNodes.value.length > 1) { approverNodes.value.splice(stepIndex, 1); } else { uni.showToast({ title: 'è³å°éè¦ä¸ä¸ªå®¡æ¹æ¥éª¤', icon: 'none' }); } }; // æ¾ç¤ºæ¥æéæ©å¨ const showDatePicker = () => { showDate.value = true } // ç¡®è®¤æ¥æéæ© const onDateConfirm = ({ selectedValues }) => { form.value.approveTime = selectedValues.join('-') currentDate.value = selectedValues showDate.value = false } // è·åå½åæ¥æå¹¶æ ¼å¼å为 YYYY-MM-DD function getCurrentDate() { const today = new Date(); const year = today.getFullYear(); const month = String(today.getMonth() + 1).padStart(2, "0"); // æä»½ä»0å¼å§ const day = String(today.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; } </script> <style scoped lang="scss"> @@ -287,74 +383,6 @@ margin-top: 16px; } .van-field { height: 56px; line-height: 36px; } .product-section { background: #fff; margin: 16px; border-radius: 16px; padding: 20px 16px 8px 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); } .section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; } .section-title { font-size: 16px; font-weight: 600; color: #333; } .add-btn { background: #2979ff; color: #fff; border-radius: 8px; padding: 4px 16px; font-size: 14px; } .product-card { background: #f8f9fa; border-radius: 12px; padding: 12px; margin-bottom: 16px; box-shadow: 0 1px 4px rgba(41, 121, 255, 0.06); position: relative; } .product-row { display: flex; align-items: center; margin-bottom: 8px; } .product-label { min-width: 60px; color: #888; font-size: 13px; } .del-row { justify-content: flex-end; } .del-btn { background: #ff4d4f; color: #fff; border-radius: 8px; padding: 4px 16px; font-size: 13px; margin-top: 4px; } .approval-process { background: #fff; margin: 16px; @@ -380,156 +408,347 @@ color: #999; } /* æ ·å¼å¢å¼ºä¸ºâç®æ´å°åå飿 ¼â */ .approval-steps { padding-left: 16px; padding-left: 22px; position: relative; &::before { content: ''; position: absolute; left: 11px; top: 40px; bottom: 40px; width: 2px; background: linear-gradient(to bottom, #e6f7ff 0%, #bae7ff 50%, #91d5ff 100%); border-radius: 1px; } } .approval-step { position: relative; margin-bottom: 20px; margin-bottom: 24px; &::before { content: ''; position: absolute; left: -18px; top: 14px; // ä» 8px è°æ´ä¸º 14pxï¼ä¸æåä¸å¿å¯¹é½ width: 12px; height: 12px; background: #fff; border: 3px solid #006cfb; border-radius: 50%; z-index: 2; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } } .step-title { top: 12px; margin-bottom: 12px; position: relative; margin-left: 6px; } .step-title text { font-size: 14px; color: #666; background: #f0f0f0; padding: 2px 8px; border-radius: 4px; } .approvers-container { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 8px; padding: 4px 12px; border-radius: 12px; position: relative; line-height: 1.4; // ç¡®ä¿æåè¡é«ä¸è´ } .approver-item { display: flex; flex-direction: column; align-items: center; width: 60px; background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); border-radius: 16px; padding: 16px; gap: 12px; position: relative; border: 1px solid #e6f7ff; box-shadow: 0 4px 12px rgba(0, 108, 251, 0.08); transition: all 0.3s ease; } .approver-avatar { width: 40px; height: 40px; background: #e6f7ff; width: 48px; height: 48px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 50%; margin-bottom: 4px; display: flex; align-items: center; justify-content: center; position: relative; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); } .approver-avatar::after { content: 'ð¤'; font-size: 20px; .avatar-text { color: #fff; font-size: 18px; font-weight: 600; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } .approver-info { flex: 1; position: relative; } .approver-name { font-size: 12px; display: block; font-size: 16px; color: #333; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 2px; font-weight: 500; position: relative; } .approver-dept { font-size: 12px; color: #999; background: rgba(0, 108, 251, 0.05); padding: 2px 8px; border-radius: 8px; display: inline-block; position: relative; &::before { content: ''; position: absolute; left: 4px; top: 50%; transform: translateY(-50%); width: 2px; height: 2px; background: #006cfb; border-radius: 50%; } } .delete-approver-btn { font-size: 12px; font-size: 16px; color: #ff4d4f; background: rgba(255, 77, 79, 0.1); width: 16px; height: 16px; background: linear-gradient(135deg, rgba(255, 77, 79, 0.1) 0%, rgba(255, 77, 79, 0.05) 100%); width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-top: 2px; transition: all 0.3s ease; position: relative; } .add-approver-btn { display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #f0f8ff 0%, #e6f7ff 100%); border: 2px dashed #006cfb; border-radius: 16px; padding: 20px; color: #006cfb; font-size: 14px; position: relative; transition: all 0.3s ease; &::before { content: ''; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 32px; height: 32px; border: 2px solid #006cfb; border-radius: 50%; opacity: 0; transition: all 0.3s ease; } } .delete-step-btn { margin-top: 8px; color: #ff4d4f; font-size: 12px; background: rgba(255, 77, 79, 0.1); padding: 2px 8px; border-radius: 4px; background: linear-gradient(135deg, rgba(255, 77, 79, 0.1) 0%, rgba(255, 77, 79, 0.05) 100%); padding: 6px 12px; border-radius: 12px; display: inline-block; position: relative; transition: all 0.3s ease; &::before { content: ''; position: absolute; left: 6px; top: 50%; transform: translateY(-50%); width: 4px; height: 4px; background: #ff4d4f; border-radius: 50%; } .add-approver-btn { width: 40px; height: 40px; border: 1px dashed #ccc; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 20px; color: #999; margin-top: 8px; } .step-line { position: absolute; left: 20px; top: 100%; width: 1px; height: 30px; background: #e0e0e0; display: none; // éè忥ç线æ¡ï¼ä½¿ç¨ä¼ªå ç´ ä»£æ¿ } .add-step-btn { display: flex; align-items: center; justify-content: center; margin-top: 16px; color: #006cfb; font-size: 14px; padding: 8px 0; border: 1px dashed #006cfb; border-radius: 8px; } .footer-btns { position: fixed; left: 0; right: 0; bottom: 0; background: #fff; display: flex; justify-content: space-around; align-items: center; padding: 12px 0; box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05); z-index: 1000; position: fixed; left: 0; right: 0; bottom: 0; background: #fff; display: flex; justify-content: space-around; align-items: center; padding: 0.75rem 0; box-shadow: 0 -0.125rem 0.5rem rgba(0,0,0,0.05); z-index: 1000; } .cancel-btn { font-weight: 400; font-size: 16px; color: #ffffff; width: 102px; background: #c7c9cc; box-shadow: 0px 4px 10px 0px rgba(3, 88, 185, 0.2); border-radius: 40px 40px 40px 40px; font-weight: 400; font-size: 1rem; color: #FFFFFF; width: 6.375rem; background: #C7C9CC; box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2); border-radius: 2.5rem 2.5rem 2.5rem 2.5rem; } .save-btn { font-weight: 400; font-size: 16px; color: #ffffff; width: 224px; background: linear-gradient(140deg, #00baff 0%, #006cfb 100%); box-shadow: 0px 4px 10px 0px rgba(3, 88, 185, 0.2); border-radius: 40px 40px 40px 40px; font-weight: 400; font-size: 1rem; color: #FFFFFF; width: 14rem; background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%); box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2); border-radius: 2.5rem 2.5rem 2.5rem 2.5rem; } // å¨ç»å®ä¹ @keyframes pulse { 0% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.2); opacity: 0.7; } 100% { transform: scale(1); opacity: 1; } } @keyframes rotate { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } @keyframes ripple { 0% { transform: translate(-50%, -50%) scale(0.8); opacity: 1; } 100% { transform: translate(-50%, -50%) scale(1.6); opacity: 0; } } /* 妿已æ .step-lineï¼è¿éæ´ç²¾åå®ä½å°å·¦ä¾§ä¸å°åç¹å¯¹é½ */ .step-line { position: absolute; left: 4px; top: 48px; width: 2px; height: calc(100% - 48px); background: #E5E7EB; } .approver-container { display: flex; align-items: center; background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); border-radius: 16px; gap: 12px; padding: 10px 0; background: transparent; border: none; box-shadow: none; } .approver-item { display: flex; align-items: center; gap: 12px; padding: 8px 10px; background: transparent; border: none; box-shadow: none; border-radius: 0; } .approver-avatar { position: relative; width: 40px; height: 40px; border-radius: 50%; background: #F3F4F6; border: 2px solid #E5E7EB; display: flex; align-items: center; justify-content: center; animation: none; /* ç¦ç¨æè½¬çå¨ç»ï¼åå½ç®æ´ */ } .avatar-text { font-size: 14px; color: #374151; font-weight: 600; } .add-approver-btn { display: flex; align-items: center; gap: 8px; background: transparent; border: none; box-shadow: none; padding: 0; } .add-approver-btn .add-circle { width: 40px; height: 40px; border: 2px dashed #A0AEC0; border-radius: 50%; color: #6B7280; display: flex; align-items: center; justify-content: center; font-size: 22px; line-height: 1; } .add-approver-btn .add-label { color: #3B82F6; font-size: 14px; } </style> src/pages/cooperativeOffice/collaborativeApproval/index.vue
@@ -8,51 +8,87 @@ <view class="search-filter-section"> <view class="search-bar"> <view class="search-input"> <u-input placeholder="请è¾å ¥éè´ååå·" class="search-text" v-model="searchKeyword"> <template #suffix> <up-icon name="search" size="24" color="#999" @click="getList"></up-icon> </template> </u-input> <input class="search-text" placeholder="请è¾å ¥æµç¨ç¼å·" v-model="searchForm.approveId" /> </view> <view class="filter-button" @click="showFilterOptions"> <van-icon name="filter-o" size="24" color="#999"></van-icon> <view class="search-button" @click="getList"> <up-icon name="search" size="24" color="#999"></up-icon> </view> </view> </view> <!-- éå®å°è´¦ç叿µ --> <view class="ledger-list" v-if="total > 0"> <!-- 审æ¹å表 --> <view class="ledger-list" v-if="ledgerList.length > 0"> <view v-for="(item, index) in ledgerList" :key="index"> <view class="ledger-item" @click="handleItemClick(item)"> <view class="ledger-item"> <view class="item-header"> <view class="item-left"> <view class="document-icon"> <up-icon name="file-text" size="16" color="#ffffff"></up-icon> </view> <text class="item-id">{{ item.salesContractNo }}</text> <text class="item-id">{{ item.approveId }}</text> </view> <view class="item-tag"> <van-tag :type="getTagClass(item.approveStatus)" size="medium">{{ formatReceiptType(item.approveStatus) }}</van-tag> </view> </view> <up-divider></up-divider> <view class="item-details"> <view class="detail-info"> <view class="detail-row"> <text class="detail-label">ç³è¯·äºº</text> <text class="detail-value">{{ item.entryPersonName }}</text> </view> <view class="detail-row"> <text class="detail-label">ç³è¯·æ¥æ</text> <text class="detail-value highlightBlue">{{ item.entryDate }}</text> </view> <view class="detail-row"> <text class="detail-label">ç³è¯·äºº</text> <text class="detail-value">{{ item.approveUserName }}</text> </view> <view class="detail-row"> <text class="detail-label">ç³è¯·é¨é¨</text> <text class="detail-value">{{ item.approveDeptName }}</text> </view> <view class="detail-row-approveReason"> <text class="detail-label">审æ¹äºç±</text> <text class="detail-value highlightBlue">{{ item.approveReason }}</text> </view> <view class="detail-row"> <text class="detail-label">ç³è¯·æ¥æ</text> <text class="detail-value">{{ item.approveTime }}</text> </view> <view class="detail-row"> <text class="detail-label">ç»ææ¥æ</text> <text class="detail-value">{{ item.approveOverTime }}</text> </view> <up-divider></up-divider> <view class="detail-info"> <view class="detail-row"> <text class="detail-label">ç³è¯·é¨é¨</text> <text class="detail-value">{{ item.entryPersonName }}</text> <view class="detail-row-user"> <text class="detail-label">å½å审æ¹äºº</text> <view class="detail-value approver-value"> <view class="approver-chip"> <text class="approver-name">{{ item.approveUserCurrentName || 'æªåé ' }}</text> </view> </view> </view> <view class="detail-row"> <text class="detail-label">审æ¹ç¶æ</text> <text class="detail-value highlightYellow">{{ item.entryDate }}</text> <view class="actions"> <van-button type="primary" size="small" class="action-btn edit" :disabled="item.approveStatus == 2 || item.approveStatus == 1 || item.approveStatus == 4" @click="handleItemClick(item)" > ç¼è¾ </van-button> <van-button type="success" size="small" class="action-btn approve" :disabled="item.approveUserCurrentId == null || item.approveStatus == 2 || item.approveStatus == 3 || item.approveStatus == 4 || item.approveUserCurrentId !== userStore.id" @click="approve(item)" > å®¡æ ¸ </van-button> </view> </view> </view> </view> @@ -62,31 +98,34 @@ <view v-else class="no-data"> <text>ææ å®¡æ¹æ°æ®</text> </view> <van-floating-bubble icon="plus" @click="handleAdd"/> <!-- æµ®å¨æä½æé® --> <view class="fab-button" @click="handleAdd"> <!-- <view class="fab-button" @click="handleAdd"> <up-icon name="plus" size="24" color="#ffffff"></up-icon> </view> </view> --> </view> </template> <script setup> import { ref, reactive, onMounted toRefs, reactive } from "vue"; import { ledgerListPage } from "@/api/cooperativeOffice/collaborativeApproval"; import PageHeader from "@/components/PageHeader.vue"; // æç´¢å ³é®è¯ const searchKeyword = ref(""); // éå®å°è´¦æ°æ® import {approveProcessListPage} from "@/api/collaborativeApproval/approvalProcess"; import {onShow} from "@dcloudio/uni-app"; import useUserStore from "@/store/modules/user"; const userStore = useUserStore() // æ°æ® const ledgerList = ref([]); const total = ref(0); const data = reactive({ searchForm: { approveId: "", }, }); const { searchForm } = toRefs(data); // è¿åä¸ä¸é¡µ const goBack = () => { @@ -98,12 +137,11 @@ current: -1, size: -1, }; ledgerListPage({ ...page approveProcessListPage({ ...page,approveType: 0,...searchForm.value }) .then((res) => { ledgerList.value = res.records; total.value = res.total; ledgerList.value = res.data.records; }) .catch(() => { // tableLoading.value = false; @@ -118,23 +156,58 @@ }, }); }; // æ ¼å¼å忬¾æ¹å¼ const formatReceiptType = (params) => { if (params == 0) { return "å¾ å®¡æ ¸"; } else if (params == 1) { return "å®¡æ ¸ä¸"; } else if (params == 2) { return "å®¡æ ¸å®æ"; } else if (params == 4) { return "已鿰æäº¤"; } else { return 'ä¸éè¿'; } }; // è·åæ ç¾æ ·å¼ç±» const getTagClass = (type) => { if (type == 0) { return "warning"; } else if (type == 1) { return "primary"; } else if (type == 2) { return "success"; } else if (type == 4) { return "primary"; } else { return "danger"; } }; // ç¹å»å表项 const handleItemClick = (item) => { uni.showToast({ title: `æ¥çåå: ${item.contractId}`, icon: "none", // ä½¿ç¨æ¬å°åå¨ä¼ éæ°æ® uni.setStorageSync('invoiceLedgerEditRow', JSON.stringify(item)); uni.navigateTo({ url: `/pages/cooperativeOffice/collaborativeApproval/detail?operationType=edit&approveId=${item.approveId}`, }); }; // æ·»å æ°è®°å½ const handleAdd = () => { uni.navigateTo({ url: "/pages/cooperativeOffice/collaborativeApproval/detail", url: "/pages/cooperativeOffice/collaborativeApproval/detail?operationType=add", }); }; // ç¹å»å®¡æ ¸ const approve = (item) => { uni.navigateTo({ url: `/pages/cooperativeOffice/collaborativeApproval/approve?approveId=${item.approveId}` }) } onMounted(() => { onShow(() => { // 页é¢å è½½å®æåçåå§åé»è¾ getList(); }); @@ -150,7 +223,27 @@ background: #f8f9fa; position: relative; } .search-input { flex: 1; background: #f5f5f5; border-radius: 24px; padding: 10px 16px; display: flex; align-items: center; gap: 8px; } .search-text { flex: 1; font-size: 14px; color: #333; background: transparent; border: none; outline: none; } .search-text::placeholder { color: #999; } .search-filter-section { @@ -163,17 +256,16 @@ align-items: center; gap: 12px; } .search-input { flex: 1; background: #f5f5f5; border-radius: 24px; padding: 4px 16px; padding: 10px 16px; display: flex; align-items: center; gap: 8px; } .search-text { flex: 1; font-size: 14px; @@ -182,7 +274,7 @@ border: none; outline: none; } .search-text::placeholder { color: #999; } @@ -239,7 +331,6 @@ } .item-tag { background: #4caf50; border-radius: 4px; padding: 2px 4px; } @@ -253,16 +344,27 @@ .item-details { padding: 16px 0; } .detail-row { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: 8px; &:last-child { margin-bottom: 0; } } .detail-row-user { display: flex; align-items: center; justify-content: space-between; } .detail-row-approveReason { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } .detail-info { @@ -316,4 +418,50 @@ box-shadow: 0 4px 16px rgba(41, 121, 255, 0.3); z-index: 1000; } .approver-value { display: flex; justify-content: flex-end; } .approver-chip { display: inline-flex; align-items: center; gap: 6px; background: #f0f6ff; color: #2b7cff; border: 1px solid #e0efff; border-radius: 999px; padding: 4px 10px; max-width: 100%; } .approver-name { font-size: 12px; color: #2b7cff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .actions { display: flex; gap: 10px; align-items: center; justify-content: flex-end; } .action-btn { border-radius: 16px; height: 28px; line-height: 28px; padding: 0 12px; } .action-btn.edit { /* primary æ ·å¼æ¥èªç»ä»¶ï¼è¿éä¿çé©å以便åç»éè¦æ©å± */ } .action-btn.approve { /* success æ ·å¼æ¥èªç»ä»¶ï¼è¿éä¿çé©å以便åç»éè¦æ©å± */ } :deep(.van-floating-bubble) { background: #ed8d05; } </style>