| | |
| | | </div> |
| | | <div> |
| | | <el-button type="primary" @click="openForm('add')">æ°å¢</el-button> |
| | | <!-- <el-button @click="handleOut">导åº</el-button>--> |
| | | <el-button @click="handleOut">导åº</el-button> |
| | | <el-button type="danger" plain @click="handleDelete">å é¤</el-button> |
| | | </div> |
| | | </div> |
| | |
| | | tableLoading.value = false; |
| | | }) |
| | | }; |
| | | // å¯¼åº |
| | | const handleOut = () => { |
| | | const type = Number(props.approveType || 0) |
| | | const urlMap = { |
| | | 0: "/approveProcess/exportZero", |
| | | 1: "/approveProcess/exportOne", |
| | | 2: "/approveProcess/exportTwo", |
| | | 3: "/approveProcess/exportThree", |
| | | 4: "/approveProcess/exportFour", |
| | | 5: "/approveProcess/exportFive", |
| | | } |
| | | const url = urlMap[type] || urlMap[0] |
| | | const nameMap = { |
| | | 0: "åå审æ¹ç®¡ç表", |
| | | 1: "å
¬åºç®¡ç审æ¹è¡¨", |
| | | 2: "请å管ç审æ¹è¡¨", |
| | | 3: "åºå·®ç®¡ç审æ¹è¡¨", |
| | | 4: "æ¥é管ç审æ¹è¡¨", |
| | | 5: "éè´ç³è¯·å®¡æ¹è¡¨", |
| | | } |
| | | const fileName = nameMap[type] || nameMap[0] |
| | | proxy.download(url, {}, `${fileName}.xlsx`) |
| | | } |
| | | // è¡¨æ ¼éæ©æ°æ® |
| | | const handleSelectionChange = (selection) => { |
| | | selectedRows.value = selection; |
| | |
| | | </el-button> |
| | | </div> |
| | | <div> |
| | | <el-button @click="handleExport" style="margin-right: 10px">导åº</el-button> |
| | | <el-button type="primary" @click="openForm('add')">æ°å¢ç¥è¯</el-button> |
| | | <el-button type="danger" plain @click="handleDelete">å é¤</el-button> |
| | | </div> |
| | |
| | | |
| | | <script setup> |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { onMounted, ref, reactive, toRefs } from "vue"; |
| | | import { onMounted, ref, reactive, toRefs, getCurrentInstance } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import PIMTable from "@/components/PIMTable/PIMTable.vue"; |
| | | import { listKnowledgeBase, delKnowledgeBase,addKnowledgeBase,updateKnowledgeBase } from "@/api/collaborativeApproval/knowledgeBase.js"; |
| | |
| | | // ç¨æ·åæ¶ |
| | | }); |
| | | }; |
| | | |
| | | // å¯¼åº |
| | | const { proxy } = getCurrentInstance() |
| | | const handleExport = () => { |
| | | proxy.download('/knowledgeBase/export', { ...searchForm.value }, 'ç¥è¯åº.xlsx') |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | |
| | | <!-- 页颿 é¢ --> |
| | | <div class="page-header"> |
| | | <h2>ä¼è®®å®¤è®¾ç½®</h2> |
| | | <el-button type="primary" @click="handleAdd"> |
| | | <div> |
| | | <el-button @click="handleExport" style="margin-right: 10px">导åº</el-button> |
| | | <el-button type="primary" @click="handleAdd"> |
| | | <el-icon><Plus /></el-icon> |
| | | æ°å¢ä¼è®®å®¤ |
| | | </el-button> |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- æç´¢åºå --> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted } from 'vue' |
| | | import { ref, reactive, onMounted, getCurrentInstance } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { Plus } from '@element-plus/icons-vue' |
| | | import Pagination from '@/components/Pagination/index.vue' |
| | |
| | | }) |
| | | } |
| | | |
| | | // å¯¼åº |
| | | const { proxy } = getCurrentInstance() |
| | | const handleExport = () => { |
| | | proxy.download('/meeting/export', { ...searchForm }, 'ä¼è®®å®¤è®¾ç½®.xlsx') |
| | | } |
| | | |
| | | // 页é¢å è½½æ¶è·åæ°æ® |
| | | onMounted(() => { |
| | | getList() |
| | |
| | | </el-button> |
| | | </div> |
| | | <div> |
| | | <el-button @click="handleExport" style="margin-right: 10px">导åº</el-button> |
| | | <el-button type="primary" @click="openForm('add')">æ°å¢</el-button> |
| | | <el-button type="danger" plain @click="handleDelete">å é¤</el-button> |
| | | </div> |
| | |
| | | |
| | | <script setup> |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { onMounted, ref, reactive, toRefs } from "vue"; |
| | | import { onMounted, ref, reactive, toRefs, getCurrentInstance } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import PIMTable from "@/components/PIMTable/PIMTable.vue"; |
| | | import {listRpa, addRpa, updateRpa, delRpa, delRpaBatch} from "@/api/collaborativeApproval/rpaManagement.js"; |
| | |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | }; |
| | | |
| | | // 导åºåè½ |
| | | const { proxy } = getCurrentInstance() |
| | | const handleExport = () => { |
| | | proxy.download('/rpaProcessAutomation/export', { ...searchForm.value }, 'RPA管ç.xlsx') |
| | | } |
| | | </script> |
| | | |
| | | <style scoped></style> |
| | |
| | | <el-col :span="8"> |
| | | <el-button type="primary" @click="searchRegulations">æç´¢</el-button> |
| | | <el-button @click="resetRegulationSearch">éç½®</el-button> |
| | | <el-button @click="handleExport">导åº</el-button> |
| | | <el-button type="success" @click="handleAdd"> |
| | | åå¸å¶åº¦ |
| | | </el-button> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted } from 'vue' |
| | | import { ref, reactive, onMounted, getCurrentInstance } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { Plus } from '@element-plus/icons-vue' |
| | | import { listSealApplication, addSealApplication, updateSealApplication,listRuleManagement,addRuleManagement,updateRuleManagement,delRuleManagement,getReadingStatusByRuleId,getReadingStatusList,addReadingStatus,updateReadingStatus } from '@/api/collaborativeApproval/sealManagement.js' |
| | |
| | | }) |
| | | } |
| | | |
| | | // 导åºè§ç« å¶åº¦ |
| | | const { proxy } = getCurrentInstance() |
| | | const handleExport = () => { |
| | | proxy.download('/rulesRegulationsManagement/export', { ...regulationSearchForm }, 'è§ç« å¶åº¦.xlsx') |
| | | } |
| | | |
| | | // è·åå°ç« ç³è¯·åè¡¨æ°æ® |
| | | const getSealApplicationList = async () => { |
| | | tableLoading.value = true |
| | |
| | | <el-col :span="8"> |
| | | <el-button type="primary" @click="searchSealApplications">æç´¢</el-button> |
| | | <el-button @click="resetSealSearch">éç½®</el-button> |
| | | <el-button @click="handleExport">导åº</el-button> |
| | | <el-button type="primary" @click="showSealApplyDialog = true">ç³è¯·ç¨å° |
| | | </el-button> |
| | | </el-col> |
| | |
| | | </el-table> |
| | | </div> |
| | | </el-card> |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | <!-- è§ç« å¶åº¦ç®¡ç --> |
| | | |
| | | <!-- <div class="tab-content"> |
| | | <el-row :gutter="20" class="mb-20"> |
| | | <el-col :span="6"> |
| | | <el-input v-model="regulationSearchForm.title" placeholder="请è¾å
¥å¶åº¦æ é¢" clearable /> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-select v-model="regulationSearchForm.category" placeholder="å¶åº¦åç±»" clearable> |
| | | <el-option label="人äºå¶åº¦" value="hr" /> |
| | | <el-option label="è´¢å¡å¶åº¦" value="finance" /> |
| | | <el-option label="å®å
¨å¶åº¦" value="safety" /> |
| | | <el-option label="ææ¯å¶åº¦" value="tech" /> |
| | | </el-select> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-button type="primary" @click="searchRegulations">æç´¢</el-button> |
| | | <el-button @click="resetRegulationSearch">éç½®</el-button> |
| | | <el-button type="success" @click="handleAdd"> |
| | | åå¸å¶åº¦ |
| | | </el-button> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-table :data="regulations" border v-loading="tableLoading" style="width: 100%"> |
| | | <el-table-column prop="regulationNum" label="å¶åº¦ç¼å·" width="120" /> |
| | | <el-table-column prop="title" label="å¶åº¦æ é¢" min-width="200" /> |
| | | <el-table-column prop="category" label="åç±»" width="120"> |
| | | <template #default="scope"> |
| | | <el-tag>{{ getCategoryText(scope.row.category) }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="version" label="çæ¬" width="80" /> |
| | | <el-table-column prop="createUserName" label="åå¸äºº" width="120" /> |
| | | <el-table-column prop="createTime" label="å叿¶é´" width="180" /> |
| | | <el-table-column prop="status" label="ç¶æ" width="100"> |
| | | <template #default="scope"> |
| | | <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'"> |
| | | {{ scope.row.status === 'active' ? 'çæä¸' : 'å·²åºæ¢' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="readCount" label="已读人æ°" width="100" /> |
| | | <el-table-column label="æä½" width="250" fixed="right"> |
| | | <template #default="scope"> |
| | | <el-button link @click="viewRegulation(scope.row)">æ¥ç</el-button> |
| | | <el-button link type="primary" @click="handleEdit(scope.row)">ç¼è¾</el-button> |
| | | <el-button link type="danger" @click="repealEdit(scope.row)">åºå¼</el-button> |
| | | <el-button link type="success" @click="viewVersionHistory(scope.row)">çæ¬åå²</el-button> |
| | | <el-button link type="warning" @click="viewReadStatus(scope.row)">é
è¯»ç¶æ</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | </div> --> |
| | | |
| | | |
| | | <!-- ç¨å°ç³è¯·å¯¹è¯æ¡ --> |
| | | <el-dialog v-model="showSealApplyDialog" title="ç³è¯·ç¨å°" width="600px"> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted } from 'vue' |
| | | import { ref, reactive, onMounted, getCurrentInstance } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { Plus } from '@element-plus/icons-vue' |
| | | import { listSealApplication, addSealApplication, updateSealApplication,listRuleManagement,addRuleManagement,updateRuleManagement,delRuleManagement,getReadingStatusByRuleId,getReadingStatusList,addReadingStatus,updateReadingStatus } from '@/api/collaborativeApproval/sealManagement.js' |
| | |
| | | }) |
| | | } |
| | | |
| | | // 导åºç¨å°ç³è¯· |
| | | const { proxy } = getCurrentInstance() |
| | | const handleExport = () => { |
| | | proxy.download('/sealApplicationManagement/export', { ...sealSearchForm }, 'ç¨å°ç³è¯·.xlsx') |
| | | } |
| | | |
| | | // è·åå°ç« ç³è¯·åè¡¨æ°æ® |
| | | const getSealApplicationList = async () => { |
| | | tableLoading.value = true |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-row :gutter="16" class="mb-16"> |
| | | <el-col :span="12"> |
| | | <el-card shadow="hover"> |
| | | <div class="section-title">ç»ä¸ä¼è®¡ç§ç®ä½ç³»</div> |
| | | <el-tree |
| | | :data="accountTree" |
| | | node-key="code" |
| | | :props="{ label: 'label', children: 'children' }" |
| | | highlight-current |
| | | default-expand-all |
| | | /> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-card shadow="hover"> |
| | | <div class="section-title">åè¯æ¨¡æ¿</div> |
| | | <el-table :data="voucherTemplates" border size="small"> |
| | | <el-table-column prop="name" label="模æ¿åç§°" min-width="140" /> |
| | | <el-table-column prop="bizScene" label="ä¸å¡åºæ¯" min-width="140" /> |
| | | <el-table-column prop="debit" label="åæ¹ç§ç®" min-width="160" /> |
| | | <el-table-column prop="credit" label="è´·æ¹ç§ç®" min-width="160" /> |
| | | <el-table-column prop="auxDims" label="è¾
婿 ¸ç®ç»´åº¦" min-width="180"> |
| | | <template #default="scope"> |
| | | <el-space wrap> |
| | | <el-tag v-for="dim in scope.row.auxDims" :key="dim" size="small" type="info">{{ dim }}</el-tag> |
| | | </el-space> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="16" class="mb-16"> |
| | | <el-col :span="12"> |
| | | <el-card shadow="hover"> |
| | | <div class="section-title">ä¸å¡æµç¨ â åè¯èªå¨çæ</div> |
| | | <div class="toolbar"> |
| | | <el-text type="info">æ¼ç¤ºæ°æ®ä»
ç¨äºå±ç¤ºç»æä¸å段</el-text> |
| | | </div> |
| | | <el-table :data="generatedVouchers" border size="small"> |
| | | <el-table-column prop="date" label="æ¥æ" width="110" /> |
| | | <el-table-column prop="bizScene" label="ä¸å¡åºæ¯" min-width="120" /> |
| | | <el-table-column prop="summary" label="æè¦" min-width="160" /> |
| | | <el-table-column prop="amount" label="éé¢(Â¥)" width="110" /> |
| | | <el-table-column prop="status" label="ç¶æ" width="100"> |
| | | <template #default="scope"> |
| | | <el-tag :type="scope.row.status === 'å·²çæ' ? 'success' : 'warning'">{{ scope.row.status }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-card shadow="hover"> |
| | | <div class="section-title">å¤ç»´è¾
婿 ¸ç®</div> |
| | | <div class="dims"> |
| | | <el-tag type="success">客æ·</el-tag> |
| | | <el-tag type="warning">项ç®</el-tag> |
| | | <el-tag type="info">é¨é¨</el-tag> |
| | | <el-tag type="primary">管çå</el-tag> |
| | | </div> |
| | | <el-table :data="auxSummary" size="small" border> |
| | | <el-table-column prop="dimension" label="维度" width="100" /> |
| | | <el-table-column prop="category" label="ç±»å«" min-width="140" /> |
| | | <el-table-column prop="debit" label="åæ¹(æ¬æ)" width="110" /> |
| | | <el-table-column prop="credit" label="è´·æ¹(æ¬æ)" width="110" /> |
| | | <el-table-column prop="balance" label="ä½é¢" width="100" /> |
| | | </el-table> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="16" class="mb-16"> |
| | | <el-col :span="12"> |
| | | <el-card shadow="hover"> |
| | | <div class="section-title">ç»è´¦ä»»å¡ï¼æ/å£/å¹´ï¼</div> |
| | | <div class="toolbar"> |
| | | <el-space> |
| | | <el-button size="small" @click="runClose('æç»')">æ§è¡æç»</el-button> |
| | | <el-button size="small" @click="runClose('壿¥')">æ§è¡å£æ¥</el-button> |
| | | <el-button size="small" @click="runClose('年度ç»è´¦')">æ§è¡å¹´åº¦ç»è´¦</el-button> |
| | | </el-space> |
| | | </div> |
| | | <el-timeline style="margin-top: 6px;"> |
| | | <el-timeline-item |
| | | v-for="item in closingTasks" |
| | | :key="item.id" |
| | | :type="item.type" |
| | | :timestamp="item.time" |
| | | placement="top" |
| | | > |
| | | <div class="close-item"> |
| | | <div class="title">{{ item.name }}</div> |
| | | <el-tag :type="item.status === '宿' ? 'success' : 'info'" size="small">{{ item.status }}</el-tag> |
| | | </div> |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-card shadow="hover"> |
| | | <div class="section-title">审计ççä¸å®¡æ¹æé</div> |
| | | <el-table :data="auditTrail" border size="small"> |
| | | <el-table-column prop="time" label="æ¶é´" width="160" /> |
| | | <el-table-column prop="action" label="å¨ä½" min-width="160" /> |
| | | <el-table-column prop="bizScene" label="å
³èä¸å¡" min-width="140" /> |
| | | <el-table-column prop="role" label="æ§è¡è§è²" width="120" /> |
| | | <el-table-column prop="result" label="ç»æ" width="100"> |
| | | <template #default="scope"> |
| | | <el-tag :type="scope.row.result === 'éè¿' ? 'success' : (scope.row.result === '驳å' ? 'danger' : 'info')"> |
| | | {{ scope.row.result }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from 'vue' |
| | | |
| | | // ç§ç®æ ï¼ç¤ºä¾ï¼ |
| | | const accountTree = ref([ |
| | | { code: '1001', label: 'èµäº§', children: [ |
| | | { code: '100101', label: 'åºåç°é' }, |
| | | { code: '100102', label: 'é¶è¡å款' }, |
| | | { code: '1122', label: 'åºæ¶è´¦æ¬¾' }, |
| | | { code: '1601', label: 'åºå®èµäº§' }, |
| | | ]}, |
| | | { code: '2001', label: 'è´åº', children: [ |
| | | { code: '2202', label: 'åºä»è´¦æ¬¾' }, |
| | | { code: '2241', label: 'å
¶ä»åºä»æ¬¾' }, |
| | | ]}, |
| | | { code: '3001', label: 'ææè
æç', children: [ |
| | | { code: '3103', label: 'æ¬å¹´å©æ¶¦' }, |
| | | ]}, |
| | | { code: '4001', label: 'ææ¬è´¹ç¨', children: [ |
| | | { code: '5601', label: 'å¶é è´¹ç¨' }, |
| | | { code: '6602', label: '管çè´¹ç¨' }, |
| | | ]}, |
| | | ]) |
| | | |
| | | // åè¯æ¨¡æ¿ï¼ç¤ºä¾ï¼ |
| | | const voucherTemplates = ref([ |
| | | { name: 'é宿¶å
¥ç¡®è®¤', bizScene: 'éå®åºåº', debit: '1122 åºæ¶è´¦æ¬¾', credit: '6001 主è¥ä¸å¡æ¶å
¥', auxDims: ['客æ·','项ç®'] }, |
| | | { name: 'éè´åºä»ç¡®è®¤', bizScene: 'éè´å
¥åº', debit: '1403 å¨éç©èµ', credit: '2202 åºä»è´¦æ¬¾', auxDims: ['项ç®','é¨é¨'] }, |
| | | { name: 'è´¹ç¨æ¥é', bizScene: 'è´¹ç¨å', debit: '6602 管çè´¹ç¨', credit: '1002 é¶è¡å款', auxDims: ['é¨é¨'] }, |
| | | { name: 'åºå®èµäº§ææ§', bizScene: 'ææ«ææ§', debit: '6602 管çè´¹ç¨', credit: '1602 ç´¯è®¡ææ§', auxDims: ['é¨é¨'] }, |
| | | ]) |
| | | |
| | | // èªå¨çæçåè¯ï¼ç¤ºä¾ï¼ |
| | | const generatedVouchers = ref([ |
| | | { date: '2025-10-01', bizScene: 'éå®åºåº', summary: 'ç¡®è®¤åºæ¶ä¸æ¶å
¥', amount: 128000, status: 'å·²çæ' }, |
| | | { date: '2025-10-03', bizScene: 'éè´å
¥åº', summary: '确认å°è´§åºä»', amount: 56000, status: 'å·²çæ' }, |
| | | { date: '2025-10-05', bizScene: 'è´¹ç¨å', summary: 'åå
¬è´¹ç¨æ¥é', amount: 3200, status: 'å·²çæ' }, |
| | | ]) |
| | | |
| | | // æ æ¨¡æçææä½ï¼ä»
å±ç¤ºéæç¤ºä¾æ°æ® |
| | | |
| | | // è¾
婿 ¸ç®ç¤ºä¾æ±æ»ï¼æ 个人å§åï¼ä»
维度类å«ï¼ |
| | | const auxSummary = ref([ |
| | | { dimension: '客æ·', category: 'éç¹å®¢æ·éå', debit: 320000, credit: 210000, balance: 110000 }, |
| | | { dimension: '项ç®', category: '项ç®A', debit: 150000, credit: 120000, balance: 30000 }, |
| | | { dimension: 'é¨é¨', category: 'è¿è¥ä¸å¿', debit: 42000, credit: 18000, balance: 24000 }, |
| | | { dimension: '管çå', category: 'ç³»ç»è§è²', debit: 0, credit: 0, balance: 0 }, |
| | | ]) |
| | | |
| | | // ç»è´¦ä»»å¡ |
| | | const closingTasks = ref([ |
| | | { id: 1, name: '2025å¹´10æ æç»', time: '2025-10-31 18:00', status: '宿', type: 'success' }, |
| | | { id: 2, name: '2025å¹´Q4 壿¥', time: '2025-12-31 18:00', status: '计å', type: 'info' }, |
| | | { id: 3, name: '2025年度 年度ç»è´¦', time: '2025-12-31 23:00', status: '计å', type: 'info' }, |
| | | ]) |
| | | |
| | | function runClose(kind) { |
| | | closingTasks.value.unshift({ |
| | | id: Date.now(), |
| | | name: `${new Date().getFullYear()}å¹´${kind}`, |
| | | time: new Date().toISOString().replace('T',' ').slice(0,16), |
| | | status: '宿', |
| | | type: 'success', |
| | | }) |
| | | } |
| | | |
| | | // 审计ççï¼ä¸å«ä¸ªäººå§åï¼ä»
è§è²/æºå¶ï¼ |
| | | const auditTrail = ref([ |
| | | { time: '2025-10-01 09:12', action: 'éå®åºåºè§¦ååè¯çæ', bizScene: 'éå®åºåº', role: 'ç³»ç»èªå¨å', result: 'éè¿' }, |
| | | { time: '2025-10-03 14:20', action: 'éè´å
¥åºè§¦ååºä»ç¡®è®¤', bizScene: 'éè´å
¥åº', role: 'ç³»ç»èªå¨å', result: 'éè¿' }, |
| | | { time: '2025-10-05 10:03', action: 'è´¹ç¨å审æ¹', bizScene: 'è´¹ç¨å', role: 'è´¢å¡å®¡æ¹', result: 'éè¿' }, |
| | | { time: '2025-10-08 16:45', action: 'åè¯è¿è´¦', bizScene: 'æ»è´¦', role: 'ä¼è®¡å®¡æ ¸', result: 'éè¿' }, |
| | | { time: '2025-10-31 18:05', action: 'æç»å®æå¹¶éè´¦', bizScene: 'æ»è´¦', role: 'ç³»ç»èªå¨å', result: 'éè¿' }, |
| | | ]) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .app-container { |
| | | padding: 16px; |
| | | } |
| | | .page-header { |
| | | margin-bottom: 12px; |
| | | h2 { margin: 0 0 6px 0; font-weight: 600; } |
| | | p { margin: 0; color: #666; } |
| | | } |
| | | .section-title { |
| | | position: relative; |
| | | padding-left: 10px; |
| | | margin-bottom: 10px; |
| | | font-weight: 700; |
| | | } |
| | | .section-title::before { |
| | | content: ''; |
| | | position: absolute; |
| | | left: 0; top: 0.2em; |
| | | width: 4px; height: 1.2em; |
| | | background: #002FA7; |
| | | border-radius: 2px; |
| | | } |
| | | .mb-16 { margin-bottom: 16px; } |
| | | .toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; } |
| | | .dims { display: flex; gap: 8px; margin-bottom: 10px; } |
| | | .close-item { display: flex; align-items: center; gap: 8px; } |
| | | </style> |
| | | |
| | | |
| | |
| | | > |
| | | </div> |
| | | <div> |
| | | <el-button @click="handleExport" style="margin-right: 10px">导åº</el-button> |
| | | <el-button type="primary" @click="openForm('add')">æ°å¢èªèµ</el-button> |
| | | <!-- <el-button @click="handleOut">导åº</el-button>--> |
| | | <el-button type="danger" plain @click="handleDelete">å é¤</el-button> |
| | | </div> |
| | | </div> |
| | |
| | | |
| | | <script setup> |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import {onMounted, ref} from "vue"; |
| | | import {onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick} from "vue"; |
| | | import FormDia from "@/views/personnelManagement/payrollManagement/components/formDia.vue"; |
| | | import {staffJoinDel} from "@/api/personnelManagement/onboarding.js"; |
| | | import {ElMessageBox} from "element-plus"; |
| | |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | }; |
| | | |
| | | // 导åºèªèµç®¡ç |
| | | const handleExport = () => { |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å¯¼åºï¼æ¯å¦ç¡®è®¤å¯¼åºï¼", "导åº", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | proxy.download("/compensationPerformance/export", { ...searchForm.value, ...page }, "èªèµç®¡ç.xlsx"); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | |
| | | <el-icon><Refresh/></el-icon> |
| | | éç½® |
| | | </el-button> |
| | | <el-button @click="handleExport"> |
| | | <el-icon><Download/></el-icon> |
| | | å¯¼åº |
| | | </el-button> |
| | | <el-button type="primary" @click="openScheduleDialog('add')"> |
| | | <el-icon><Plus/></el-icon> |
| | | æ°å¢æç |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import {ref, reactive, computed, onMounted} from 'vue' |
| | | import {ref, reactive, computed, onMounted, getCurrentInstance} from 'vue' |
| | | import {ElMessage, ElMessageBox} from 'element-plus' |
| | | import {useDict} from "@/utils/dict.js" |
| | | import {Plus, Download, Search, Refresh} from '@element-plus/icons-vue' |
| | |
| | | import {getStaffOnJob} from "@/api/personnelManagement/onboarding.js"; |
| | | import dayjs from "dayjs"; |
| | | import pagination from "@/components/PIMTable/Pagination.vue"; |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const tableCount = ref(0) |
| | | // ååºå¼æ°æ® |
| | |
| | | selectedRows.value = selection |
| | | } |
| | | |
| | | // å¯¼åº |
| | | const handleExport = () => { |
| | | let searchForm = { |
| | | ...filterForm, |
| | | ...(filterForm.dateRange.length > 0 && { |
| | | startDate: filterForm.dateRange[0], |
| | | endDate: filterForm.dateRange[1], |
| | | }) |
| | | } |
| | | proxy.download('/staff/staffScheduling/export', {}, '人åæç.xlsx') |
| | | } |
| | | |
| | | // çå½å¨æ |
| | | onMounted(() => { |
| | |
| | | <div class="actions"> |
| | | <div></div> |
| | | <div> |
| | | <el-button @click="handleExport" style="margin-right: 10px">导åº</el-button> |
| | | <el-button type="primary" @click="handleAdd('add')"> |
| | | æ°å¢ç»è®° |
| | | </el-button> |
| | | <!-- <el-button @click="handleOut">导åº</el-button>--> |
| | | <!-- <el-button type="danger" plain @click="handleDelete">å é¤</el-button>--> |
| | | </div> |
| | | </div> |
| | |
| | | <script setup> |
| | | import { usePaginationApi } from "@/hooks/usePaginationApi"; |
| | | import {delRegistration, gePurchaseListPage} from "@/api/procurementManagement/invoiceEntry.js"; |
| | | import { nextTick, onMounted, getCurrentInstance } from "vue"; |
| | | import { nextTick, onMounted, getCurrentInstance, ref } from "vue"; |
| | | import ExpandTable from "./components/ExpandTable.vue"; |
| | | import Modal from "./components/Modal.vue"; |
| | | import {ElMessageBox} from "element-plus"; |
| | |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | }; |
| | | |
| | | // 导åºéè´å°è´¦ |
| | | const handleExport = () => { |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å¯¼åºï¼æ¯å¦ç¡®è®¤å¯¼åºï¼", "导åº", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | proxy.download("/purchase/ledger/exportOne", {}, "æ¥ç¥¨ç»è®°.xlsx"); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | }; |
| | | |
| | | // å é¤ |
| | | const handleDelete = () => { |
| | | let ids = []; |
| | |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-form-item style="float: right; margin-right: unset"> |
| | | <el-button @click="handleExport" style="margin-right: 10px">导åº</el-button> |
| | | <el-button type="primary" @click="openForm('add')"> |
| | | æ°å¢ä»æ¬¾ |
| | | </el-button> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from "vue"; |
| | | import { ref, reactive, toRefs, getCurrentInstance, nextTick, onMounted } from "vue"; |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import useUserStore from "@/store/modules/user.js"; |
| | |
| | | const day = String(today.getDate()).padStart(2, "0"); |
| | | return `${year}-${month}-${day}`; |
| | | } |
| | | getList(); |
| | | |
| | | // å¯¼åº |
| | | const handleExport = () => { |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å¯¼åºï¼æ¯å¦ç¡®è®¤å¯¼åºï¼", "导åº", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | proxy.download("/purchase/registration/exportOne", { ...searchForm, ...page }, "仿¬¾ç»è®°.xlsx"); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | |
| | | > |
| | | æç´¢ |
| | | </el-button> |
| | | <el-button @click="handleExport">导åº</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div class="table_list"> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from "vue"; |
| | | import { ref, reactive, getCurrentInstance, onMounted } from "vue"; |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { paymentHistoryListPage } from "@/api/procurementManagement/paymentEntry.js"; |
| | | import useFormData from "@/hooks/useFormData"; |
| | |
| | | const total = ref(0); |
| | | const { form: searchForm } = useFormData({ |
| | | searchText: undefined, |
| | | paymentDate: [ |
| | | dayjs().startOf("month").format("YYYY-MM-DD"), |
| | | dayjs().endOf("month").format("YYYY-MM-DD"), |
| | | ], |
| | | paymentDateStart: dayjs().startOf("month").format("YYYY-MM-DD"), |
| | | paymentDateEnd: dayjs().endOf("month").format("YYYY-MM-DD"), |
| | | paymentDate: [], |
| | | paymentDateStart: undefined, |
| | | paymentDateEnd: undefined, |
| | | }); |
| | | |
| | | // æ¥è¯¢å表 |
| | |
| | | getList(); |
| | | }; |
| | | |
| | | // å¯¼åº |
| | | const handleExport = () => { |
| | | const { paymentDate, ...rest } = searchForm; |
| | | proxy.download("/purchase/paymentRegistration/export", { ...rest, ...page }, "仿¬¾æµæ°´.xlsx"); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container indicator-stats"> |
| | | <el-card class="box-card"> |
| | | <!-- KPI æ±æ» --> |
| | | <el-row :gutter="20" class="stats-row"> |
| | | <el-col :span="6"> |
| | | <div class="stat-card"> |
| | | <div class="stat-icon" style="background: #ecf5ff;"> |
| | | <el-icon :size="30" color="#409eff"><Document /></el-icon> |
| | | </div> |
| | | <div class="stat-content"> |
| | | <div class="stat-value">{{ indicatorKpis.orderCount.toLocaleString() }}</div> |
| | | <div class="stat-label">è®¢åæ°é</div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="stat-card"> |
| | | <div class="stat-icon" style="background: #f0f9ff;"> |
| | | <el-icon :size="30" color="#67c23a"><Tickets /></el-icon> |
| | | </div> |
| | | <div class="stat-content"> |
| | | <div class="stat-value">Â¥{{ indicatorKpis.salesAmount.toLocaleString() }}</div> |
| | | <div class="stat-label">éå®é¢</div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="stat-card"> |
| | | <div class="stat-icon" style="background: #fef0f0;"> |
| | | <el-icon :size="30" color="#e6a23c"><Van /></el-icon> |
| | | </div> |
| | | <div class="stat-content"> |
| | | <div class="stat-value">{{ indicatorKpis.shipmentRate }}%</div> |
| | | <div class="stat-label">åè´§ç</div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="stat-card"> |
| | | <div class="stat-icon" style="background: #f4f4f5;"> |
| | | <el-icon :size="30" color="#f56c6c"><Wallet /></el-icon> |
| | | </div> |
| | | <div class="stat-content"> |
| | | <div class="stat-value">{{ indicatorKpis.collectionRate }}%</div> |
| | | <div class="stat-label">忬¾ç</div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 维度çé --> |
| | | <el-row :gutter="20" class="search-row"> |
| | | <el-col :span="6"> |
| | | <el-select v-model="indicatorFilter.product" placeholder="产å" clearable> |
| | | <el-option label="å
¨é¨äº§å" value="" /> |
| | | <el-option label="P.O 42.5æ®éç¡
é
¸çæ°´æ³¥" value="P.O 42.5æ®éç¡
é
¸çæ°´æ³¥" /> |
| | | <el-option label="P.S 32.5ç¿æ¸£ç¡
é
¸çæ°´æ³¥" value="P.S 32.5ç¿æ¸£ç¡
é
¸çæ°´æ³¥" /> |
| | | <el-option label="P.C 32.5å¤åç¡
é
¸çæ°´æ³¥" value="P.C 32.5å¤åç¡
é
¸çæ°´æ³¥" /> |
| | | </el-select> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-select v-model="indicatorFilter.customer" placeholder="客æ·" clearable> |
| | | <el-option label="å
¨é¨å®¢æ·" value="" /> |
| | | <el-option label="åä¸å»ºæéå¢" value="åä¸å»ºæéå¢" /> |
| | | <el-option label="é¿æ±æ··ååå
¬å¸" value="é¿æ±æ··ååå
¬å¸" /> |
| | | <el-option label="æµ¦æ±æ°´æ³¥å¶åå" value="æµ¦æ±æ°´æ³¥å¶åå" /> |
| | | </el-select> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-select v-model="indicatorFilter.region" placeholder="åºå" clearable> |
| | | <el-option label="å
¨é¨åºå" value="" /> |
| | | <el-option label="åä¸å°åº" value="åä¸å°åº" /> |
| | | <el-option label="ååå°åº" value="ååå°åº" /> |
| | | <el-option label="ååå°åº" value="ååå°åº" /> |
| | | </el-select> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-date-picker v-model="indicatorFilter.dateRange" type="daterange" range-separator="è³" |
| | | start-placeholder="å¼å§æ¥æ" end-placeholder="ç»ææ¥æ" value-format="YYYY-MM-DD" style="width: 100%" /> |
| | | </el-col> |
| | | <el-col :span="24" style="text-align: right; margin-top: 10px;"> |
| | | <el-button type="primary" @click="applyIndicatorFilter">æ¥è¯¢</el-button> |
| | | <el-button @click="resetIndicatorFilter">éç½®</el-button> |
| | | <el-button @click="exportIndicatorTable">å¯¼åºæ¥è¡¨</el-button> |
| | | <el-button @click="exportIndicatorChart">导åºå¾è¡¨</el-button> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- å¾è¡¨åº --> |
| | | <div class="chart-container"> |
| | | <div ref="indicatorChartRef" style="width: 100%; height: 360px;"></div> |
| | | </div> |
| | | |
| | | <!-- ä¸ç»©ç»è®¡ï¼å¢éç»´åº¦ï¼æ 个人å§åï¼ --> |
| | | <el-table :data="teamPerformanceList" border stripe style="margin-top: 20px;"> |
| | | <el-table-column prop="team" label="éå®å¢é"/> |
| | | <el-table-column prop="orderCount" label="è®¢åæ°"/> |
| | | <el-table-column prop="salesAmount" label="éå®é¢"> |
| | | <template #default="scope">Â¥{{ scope.row.salesAmount.toLocaleString() }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="shipmentRate" label="åè´§ç"> |
| | | <template #default="scope">{{ scope.row.shipmentRate }}%</template> |
| | | </el-table-column> |
| | | <el-table-column prop="collectionRate" label="忬¾ç"> |
| | | <template #default="scope">{{ scope.row.collectionRate }}%</template> |
| | | </el-table-column> |
| | | <el-table-column prop="attainment" label="ç®æ è¾¾æç"> |
| | | <template #default="scope"> |
| | | <el-tag :type="scope.row.attainment >= 100 ? 'success' : scope.row.attainment >= 80 ? 'warning' : 'danger'"> |
| | | {{ scope.row.attainment }}% |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, nextTick } from 'vue' |
| | | import { Document, Van, Tickets, Wallet } from '@element-plus/icons-vue' |
| | | import * as echarts from 'echarts' |
| | | |
| | | const indicatorKpis = reactive({ |
| | | orderCount: 1280, |
| | | salesAmount: 9650000, |
| | | shipmentRate: 89.2, |
| | | collectionRate: 76.4 |
| | | }) |
| | | |
| | | const indicatorFilter = reactive({ |
| | | product: '', |
| | | customer: '', |
| | | region: '', |
| | | dateRange: [] |
| | | }) |
| | | |
| | | const indicatorChartRef = ref(null) |
| | | let indicatorChart = null |
| | | |
| | | const teamPerformanceList = ref([ |
| | | { team: 'åä¸å¤§åº', orderCount: 320, salesAmount: 2850000, shipmentRate: 90, collectionRate: 80, attainment: 105 }, |
| | | { team: 'åå大åº', orderCount: 280, salesAmount: 2150000, shipmentRate: 86, collectionRate: 73, attainment: 92 }, |
| | | { team: 'åå大åº', orderCount: 210, salesAmount: 1850000, shipmentRate: 88, collectionRate: 70, attainment: 78 }, |
| | | { team: '西å大åº', orderCount: 180, salesAmount: 1500000, shipmentRate: 83, collectionRate: 68, attainment: 74 } |
| | | ]) |
| | | |
| | | const initIndicatorChart = () => { |
| | | if (!indicatorChartRef.value) return |
| | | if (indicatorChart) indicatorChart.dispose() |
| | | indicatorChart = echarts.init(indicatorChartRef.value) |
| | | const option = { |
| | | title: { text: 'å¤ç»´åº¦é宿æ è¶å¿', left: 'center' }, |
| | | tooltip: { trigger: 'axis' }, |
| | | legend: { data: ['è®¢åæ°', 'éå®é¢', 'åè´§ç', '忬¾ç'], top: 30 }, |
| | | grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, |
| | | xAxis: { type: 'category', data: ['2024-12', '2025-01', '2025-02', '2025-03', '2025-04', '2025-05'] }, |
| | | yAxis: [ |
| | | { type: 'value', name: 'æ°é/éé¢', axisLabel: { formatter: '{value}' } }, |
| | | { type: 'value', name: 'æ¯ä¾(%)', min: 0, max: 100, axisLabel: { formatter: '{value}%' } } |
| | | ], |
| | | series: [ |
| | | { name: 'è®¢åæ°', type: 'bar', data: [180, 220, 210, 260, 205, 225], itemStyle: { color: '#409eff' } }, |
| | | { name: 'éå®é¢', type: 'bar', data: [820, 950, 910, 1080, 980, 1020], itemStyle: { color: '#67c23a' } }, |
| | | { name: 'åè´§ç', type: 'line', yAxisIndex: 1, data: [86, 89, 88, 91, 87, 90], itemStyle: { color: '#e6a23c' } }, |
| | | { name: '忬¾ç', type: 'line', yAxisIndex: 1, data: [72, 76, 74, 79, 75, 78], itemStyle: { color: '#f56c6c' } } |
| | | ] |
| | | } |
| | | indicatorChart.setOption(option) |
| | | } |
| | | |
| | | const applyIndicatorFilter = () => { |
| | | const random = (base, delta) => { |
| | | const v = base + Math.round((Math.random() - 0.5) * delta) |
| | | return v < 0 ? 0 : v |
| | | } |
| | | indicatorKpis.orderCount = random(1280, 120) |
| | | indicatorKpis.salesAmount = random(9650000, 350000) |
| | | indicatorKpis.shipmentRate = (85 + Math.random() * 10).toFixed(1) * 1 |
| | | indicatorKpis.collectionRate = (70 + Math.random() * 12).toFixed(1) * 1 |
| | | setTimeout(() => initIndicatorChart(), 200) |
| | | } |
| | | |
| | | const resetIndicatorFilter = () => { |
| | | indicatorFilter.product = '' |
| | | indicatorFilter.customer = '' |
| | | indicatorFilter.region = '' |
| | | indicatorFilter.dateRange = [] |
| | | applyIndicatorFilter() |
| | | } |
| | | |
| | | const exportIndicatorTable = () => { |
| | | const header = ['éå®å¢é', 'è®¢åæ°', 'éå®é¢', 'åè´§ç(%)', '忬¾ç(%)', 'ç®æ è¾¾æç(%)'] |
| | | const rows = teamPerformanceList.value.map(r => [ |
| | | r.team, |
| | | r.orderCount, |
| | | r.salesAmount, |
| | | r.shipmentRate, |
| | | r.collectionRate, |
| | | r.attainment |
| | | ]) |
| | | const csv = [header, ...rows].map(r => r.join(',')).join('\n') |
| | | const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }) |
| | | const url = URL.createObjectURL(blob) |
| | | const link = document.createElement('a') |
| | | link.href = url |
| | | link.download = 'ææ ç»è®¡-å¢éä¸ç»©.csv' |
| | | document.body.appendChild(link) |
| | | link.click() |
| | | document.body.removeChild(link) |
| | | URL.revokeObjectURL(url) |
| | | } |
| | | |
| | | const exportIndicatorChart = () => { |
| | | if (!indicatorChart) return |
| | | const url = indicatorChart.getDataURL({ type: 'png', pixelRatio: 2, backgroundColor: '#fff' }) |
| | | const link = document.createElement('a') |
| | | link.href = url |
| | | link.download = 'ææ ç»è®¡-å¾è¡¨.png' |
| | | document.body.appendChild(link) |
| | | link.click() |
| | | document.body.removeChild(link) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | nextTick(() => initIndicatorChart()) |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .indicator-stats { |
| | | padding: 0; |
| | | } |
| | | .box-card { border: none; box-shadow: none; } |
| | | .search-row { margin-bottom: 20px; } |
| | | .stats-row { margin-bottom: 24px; } |
| | | .stat-card { display: flex; align-items: center; padding: 20px; background: #fff; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } |
| | | .stat-icon { width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; border-radius: 8px; margin-right: 16px; } |
| | | .stat-content { flex: 1; } |
| | | .stat-value { font-size: 28px; font-weight: bold; color: #303133; margin-bottom: 4px; } |
| | | .stat-label { font-size: 14px; color: #909399; } |
| | | .chart-container { margin: 20px 0; padding: 20px; background: #fff; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } |
| | | </style> |
| | | |
| | | |
| | |
| | | <el-form-item> |
| | | <el-button type="primary" @click="handleQuery"> æç´¢ </el-button> |
| | | <el-button @click="resetForm"> éç½® </el-button> |
| | | <el-button @click="handleExport" style="margin-right: 10px">导åº</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | <div class="table_list"> |
| | | <div class="flex justify-between"> |
| | | <div></div> |
| | | <div> |
| | | <el-button type="primary" @click="openForm" style="margin-bottom: 8px"> |
| | | æ°å¢ç»è®° |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | <el-table |
| | | :data="tableData" |
| | |
| | | }); |
| | | }; |
| | | |
| | | // 导åºéå®å°è´¦ |
| | | const handleExport = () => { |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å¯¼åºï¼æ¯å¦ç¡®è®¤å¯¼åºï¼", "导åº", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | proxy.download("/sales/ledger/exportOne", { ...searchForm, ...page }, "å¼ç¥¨ç»è®°.xlsx"); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | }; |
| | | |
| | | //æ¬æ¬¡å¼ç¥¨å¤±ç¦æä½ |
| | | const invoiceNumBlur = (row) => { |
| | | if (!row.currentInvoiceNum) { |
| | |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="handleQuery"> æç´¢ </el-button> |
| | | <el-button @click="handleExport">导åº</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div class="table_list"> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from "vue"; |
| | | import { ref, reactive, getCurrentInstance } from "vue"; |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { receiptPaymentHistoryListPage } from "@/api/salesManagement/receiptPayment.js"; |
| | | import useFormData from "@/hooks/useFormData"; |
| | |
| | | |
| | | const { form: searchForm } = useFormData({ |
| | | searchText: undefined, |
| | | receiptPaymentDate: [ |
| | | dayjs().startOf("month").format("YYYY-MM-DD"), |
| | | dayjs().endOf("month").format("YYYY-MM-DD"), |
| | | ], |
| | | receiptPaymentDateStart: dayjs() |
| | | .startOf("month") |
| | | .format("YYYY-MM-DD 00:00:00"), |
| | | receiptPaymentDateEnd: dayjs().endOf("month").format("YYYY-MM-DD 23:59:59"), |
| | | receiptPaymentDate: [], |
| | | receiptPaymentDateStart: undefined, |
| | | receiptPaymentDateEnd: undefined, |
| | | customerContractNo: undefined, |
| | | projectName: undefined, |
| | | }); |
| | |
| | | getList(); |
| | | }; |
| | | |
| | | // å¯¼åº |
| | | const handleExport = () => { |
| | | const { receiptPaymentDate, ...rest } = searchForm; |
| | | proxy.download("/receiptPayment/exportOne", { ...rest, ...page }, "忬¾æµæ°´.xlsx"); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | |
| | | /> |
| | | </el-card> |
| | | </el-tab-pane> |
| | | |
| | | <!-- ææ ç»è®¡ï¼å¤ç»´åº¦éå®åæï¼ --> |
| | | <el-tab-pane label="ææ ç»è®¡" name="indicatorStats"> |
| | | <el-card class="box-card"> |
| | | <!-- KPI æ±æ» --> |
| | | <el-row :gutter="20" class="stats-row"> |
| | | <el-col :span="6"> |
| | | <div class="stat-card"> |
| | | <div class="stat-icon" style="background: #ecf5ff;"> |
| | | <el-icon :size="30" color="#409eff"><Document /></el-icon> |
| | | </div> |
| | | <div class="stat-content"> |
| | | <div class="stat-value">{{ indicatorKpis.orderCount.toLocaleString() }}</div> |
| | | <div class="stat-label">è®¢åæ°é</div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="stat-card"> |
| | | <div class="stat-icon" style="background: #f0f9ff;"> |
| | | <el-icon :size="30" color="#67c23a"><Tickets /></el-icon> |
| | | </div> |
| | | <div class="stat-content"> |
| | | <div class="stat-value">Â¥{{ indicatorKpis.salesAmount.toLocaleString() }}</div> |
| | | <div class="stat-label">éå®é¢</div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="stat-card"> |
| | | <div class="stat-icon" style="background: #fef0f0;"> |
| | | <el-icon :size="30" color="#e6a23c"><Van /></el-icon> |
| | | </div> |
| | | <div class="stat-content"> |
| | | <div class="stat-value">{{ indicatorKpis.shipmentRate }}%</div> |
| | | <div class="stat-label">åè´§ç</div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="stat-card"> |
| | | <div class="stat-icon" style="background: #f4f4f5;"> |
| | | <el-icon :size="30" color="#f56c6c"><Wallet /></el-icon> |
| | | </div> |
| | | <div class="stat-content"> |
| | | <div class="stat-value">{{ indicatorKpis.collectionRate }}%</div> |
| | | <div class="stat-label">忬¾ç</div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 维度çé --> |
| | | <el-row :gutter="20" class="search-row"> |
| | | <el-col :span="6"> |
| | | <el-select v-model="indicatorFilter.product" placeholder="产å" clearable> |
| | | <el-option label="å
¨é¨äº§å" value="" /> |
| | | <el-option label="P.O 42.5æ®éç¡
é
¸çæ°´æ³¥" value="P.O 42.5æ®éç¡
é
¸çæ°´æ³¥" /> |
| | | <el-option label="P.S 32.5ç¿æ¸£ç¡
é
¸çæ°´æ³¥" value="P.S 32.5ç¿æ¸£ç¡
é
¸çæ°´æ³¥" /> |
| | | <el-option label="P.C 32.5å¤åç¡
é
¸çæ°´æ³¥" value="P.C 32.5å¤åç¡
é
¸çæ°´æ³¥" /> |
| | | </el-select> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-select v-model="indicatorFilter.customer" placeholder="客æ·" clearable> |
| | | <el-option label="å
¨é¨å®¢æ·" value="" /> |
| | | <el-option label="åä¸å»ºæéå¢" value="åä¸å»ºæéå¢" /> |
| | | <el-option label="é¿æ±æ··ååå
¬å¸" value="é¿æ±æ··ååå
¬å¸" /> |
| | | <el-option label="æµ¦æ±æ°´æ³¥å¶åå" value="æµ¦æ±æ°´æ³¥å¶åå" /> |
| | | </el-select> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-select v-model="indicatorFilter.region" placeholder="åºå" clearable> |
| | | <el-option label="å
¨é¨åºå" value="" /> |
| | | <el-option label="åä¸å°åº" value="åä¸å°åº" /> |
| | | <el-option label="ååå°åº" value="ååå°åº" /> |
| | | <el-option label="ååå°åº" value="ååå°åº" /> |
| | | </el-select> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-date-picker v-model="indicatorFilter.dateRange" type="daterange" range-separator="è³" |
| | | start-placeholder="å¼å§æ¥æ" end-placeholder="ç»ææ¥æ" value-format="YYYY-MM-DD" style="width: 100%" /> |
| | | </el-col> |
| | | <el-col :span="24" style="text-align: right; margin-top: 10px;"> |
| | | <el-button type="primary" @click="applyIndicatorFilter">æ¥è¯¢</el-button> |
| | | <el-button @click="resetIndicatorFilter">éç½®</el-button> |
| | | <el-button @click="exportIndicatorTable">å¯¼åºæ¥è¡¨</el-button> |
| | | <el-button @click="exportIndicatorChart">导åºå¾è¡¨</el-button> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- å¾è¡¨åº --> |
| | | <div class="chart-container"> |
| | | <div ref="indicatorChartRef" style="width: 100%; height: 360px;"></div> |
| | | </div> |
| | | |
| | | <!-- ä¸ç»©ç»è®¡ï¼å¢éç»´åº¦ï¼æ 个人å§åï¼ --> |
| | | <el-table :data="teamPerformanceList" border stripe style="margin-top: 20px;"> |
| | | <el-table-column prop="team" label="éå®å¢é" width="140" /> |
| | | <el-table-column prop="orderCount" label="è®¢åæ°" width="100" /> |
| | | <el-table-column prop="salesAmount" label="éå®é¢" width="140"> |
| | | <template #default="scope">Â¥{{ scope.row.salesAmount.toLocaleString() }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="shipmentRate" label="åè´§ç" width="100"> |
| | | <template #default="scope">{{ scope.row.shipmentRate }}%</template> |
| | | </el-table-column> |
| | | <el-table-column prop="collectionRate" label="忬¾ç" width="100"> |
| | | <template #default="scope">{{ scope.row.collectionRate }}%</template> |
| | | </el-table-column> |
| | | <el-table-column prop="attainment" label="ç®æ è¾¾æç" width="120"> |
| | | <template #default="scope"> |
| | | <el-tag :type="scope.row.attainment >= 100 ? 'success' : scope.row.attainment >= 80 ? 'warning' : 'danger'"> |
| | | {{ scope.row.attainment }}% |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | |
| | | <!-- ä»·æ ¼çç¥å¯¹è¯æ¡ --> |
| | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="çç¥ç±»å" prop="strategyType"> |
| | | <el-select v-model="priceStrategyForm.strategyType" placeholder="è¯·éæ©çç¥ç±»å" style="width: 100%;"> |
| | | <el-select v-model="priceStrategyForm.strategyType" placeholder="è¯·éæ©çç¥ç±»å" style="width: 100%;" :disabled="priceStrategyDialogMode === 'view'"> |
| | | <el-option label="ä¸å±ä»·æ ¼" value="ä¸å±ä»·æ ¼"></el-option> |
| | | <el-option label="é¶æ¢¯æ¥ä»·" value="é¶æ¢¯æ¥ä»·"></el-option> |
| | | <el-option label="ä¿éææ£" value="ä¿éææ£"></el-option> |
| | |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="客æ·åç§°" prop="customerName"> |
| | | <el-select v-model="priceStrategyForm.customerName" placeholder="è¯·éæ©å®¢æ·" style="width: 100%;"> |
| | | <el-select v-model="priceStrategyForm.customerName" placeholder="è¯·éæ©å®¢æ·" style="width: 100%;" :disabled="priceStrategyDialogMode === 'view'"> |
| | | <el-option label="åä¸å»ºæéå¢" value="åä¸å»ºæéå¢"></el-option> |
| | | <el-option label="é¿æ±æ··ååå
¬å¸" value="é¿æ±æ··ååå
¬å¸"></el-option> |
| | | <el-option label="æµ¦æ±æ°´æ³¥å¶åå" value="æµ¦æ±æ°´æ³¥å¶åå"></el-option> |
| | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="产ååç§°" prop="productName"> |
| | | <el-select v-model="priceStrategyForm.productName" placeholder="è¯·éæ©äº§å" style="width: 100%;"> |
| | | <el-select v-model="priceStrategyForm.productName" placeholder="è¯·éæ©äº§å" style="width: 100%;" :disabled="priceStrategyDialogMode === 'view'"> |
| | | <el-option label="P.O 42.5æ®éç¡
é
¸çæ°´æ³¥" value="P.O 42.5æ®éç¡
é
¸çæ°´æ³¥"></el-option> |
| | | <el-option label="P.S 32.5ç¿æ¸£ç¡
é
¸çæ°´æ³¥" value="P.S 32.5ç¿æ¸£ç¡
é
¸çæ°´æ³¥"></el-option> |
| | | <el-option label="P.C 32.5å¤åç¡
é
¸çæ°´æ³¥" value="P.C 32.5å¤åç¡
é
¸çæ°´æ³¥"></el-option> |
| | |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è§æ ¼åå·" prop="specification"> |
| | | <el-input v-model="priceStrategyForm.specification" placeholder="请è¾å
¥è§æ ¼åå·" /> |
| | | <el-input v-model="priceStrategyForm.specification" placeholder="请è¾å
¥è§æ ¼åå·" :disabled="priceStrategyDialogMode === 'view'" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åºç¡ä»·æ ¼(å
/å¨)" prop="basePrice"> |
| | | <el-input-number v-model="priceStrategyForm.basePrice" :min="0" :precision="2" style="width: 100%;" /> |
| | | <el-input-number v-model="priceStrategyForm.basePrice" :min="0" :precision="2" style="width: 100%;" :disabled="priceStrategyDialogMode === 'view'" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="çç¥ä»·æ ¼" prop="strategyPrice"> |
| | | <el-input v-model="priceStrategyForm.strategyPrice" placeholder="å¦: Â¥350/å¨ æ 9æ" /> |
| | | <el-input v-model="priceStrategyForm.strategyPrice" placeholder="å¦: Â¥350/å¨ æ 9æ" :disabled="priceStrategyDialogMode === 'view'" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | placeholder="éæ©çææ¥æ" |
| | | style="width: 100%" |
| | | value-format="YYYY-MM-DD" |
| | | :disabled="priceStrategyDialogMode === 'view'" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | placeholder="éæ©å¤±ææ¥æ" |
| | | style="width: 100%" |
| | | value-format="YYYY-MM-DD" |
| | | :disabled="priceStrategyDialogMode === 'view'" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-form-item label="çç¥è¯´æ" prop="description"> |
| | | <el-input type="textarea" v-model="priceStrategyForm.description" :rows="3" placeholder="请è¾å
¥çç¥è¯´æ" /> |
| | | <el-input type="textarea" v-model="priceStrategyForm.description" :rows="3" placeholder="请è¾å
¥çç¥è¯´æ" :disabled="priceStrategyDialogMode === 'view'" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button @click="priceStrategyDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" @click="handleSavePriceStrategy">ä¿å</el-button> |
| | | <el-button @click="priceStrategyDialogVisible = false">{{ priceStrategyDialogMode === 'view' ? 'å
³é' : 'åæ¶' }}</el-button> |
| | | <el-button v-if="priceStrategyDialogMode !== 'view'" type="primary" @click="handleSavePriceStrategy">ä¿å</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | |
| | | |
| | | const priceStrategyDialogVisible = ref(false) |
| | | const priceStrategyDialogTitle = ref('æ°å¢ä»·æ ¼çç¥') |
| | | const priceStrategyDialogMode = ref('add') // add | edit | view |
| | | const priceStrategyForm = reactive({ |
| | | strategyType: '', |
| | | customerName: '', |
| | |
| | | const handleAddPriceStrategy = () => { |
| | | priceStrategyDialogTitle.value = 'æ°å¢ä»·æ ¼çç¥' |
| | | resetPriceStrategyForm() |
| | | priceStrategyDialogMode.value = 'add' |
| | | priceStrategyDialogVisible.value = true |
| | | } |
| | | |
| | | const handleViewPriceStrategy = (row) => { |
| | | ElMessage.info('æ¥ççç¥è¯¦æ
: ' + row.strategyNo) |
| | | priceStrategyDialogTitle.value = 'æ¥çä»·æ ¼çç¥' |
| | | Object.assign(priceStrategyForm, row) |
| | | priceStrategyDialogMode.value = 'view' |
| | | priceStrategyDialogVisible.value = true |
| | | } |
| | | |
| | | const handleEditPriceStrategy = (row) => { |
| | | priceStrategyDialogTitle.value = 'ç¼è¾ä»·æ ¼çç¥' |
| | | Object.assign(priceStrategyForm, row) |
| | | priceStrategyDialogMode.value = 'edit' |
| | | priceStrategyDialogVisible.value = true |
| | | } |
| | | |
| | |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | }).then(() => { |
| | | // æ¬å°åæ°æ®å é¤ï¼ä»å表ä¸ç§»é¤å¹¶æ´æ°æ»æ° |
| | | const index = priceStrategyList.value.findIndex(item => item.id === row.id) |
| | | if (index > -1) { |
| | | priceStrategyList.value.splice(index, 1) |
| | | if (pricePagination.total > 0) pricePagination.total -= 1 |
| | | } |
| | | ElMessage.success('å 餿å') |
| | | }) |
| | | } |
| | |
| | | profitPagination.pageSize = val.limit |
| | | } |
| | | |
| | | // ========== ææ ç»è®¡ï¼å¤ç»´åº¦åæï¼ ========== |
| | | const indicatorKpis = reactive({ |
| | | orderCount: 1280, |
| | | salesAmount: 9650000, |
| | | shipmentRate: 89.2, |
| | | collectionRate: 76.4 |
| | | }) |
| | | |
| | | const indicatorFilter = reactive({ |
| | | product: '', |
| | | customer: '', |
| | | region: '', |
| | | dateRange: [] |
| | | }) |
| | | |
| | | const indicatorChartRef = ref(null) |
| | | let indicatorChart = null |
| | | |
| | | const teamPerformanceList = ref([ |
| | | { team: 'åä¸å¢éA', orderCount: 320, salesAmount: 2850000, shipmentRate: 90, collectionRate: 80, attainment: 105 }, |
| | | { team: 'ååå¢éB', orderCount: 280, salesAmount: 2150000, shipmentRate: 86, collectionRate: 73, attainment: 92 }, |
| | | { team: 'ååå¢éC', orderCount: 210, salesAmount: 1850000, shipmentRate: 88, collectionRate: 70, attainment: 78 }, |
| | | { team: '西åå¢éD', orderCount: 180, salesAmount: 1500000, shipmentRate: 83, collectionRate: 68, attainment: 74 } |
| | | ]) |
| | | |
| | | const initIndicatorChart = () => { |
| | | if (!indicatorChartRef.value) return |
| | | if (indicatorChart) indicatorChart.dispose() |
| | | indicatorChart = echarts.init(indicatorChartRef.value) |
| | | const option = { |
| | | title: { text: 'å¤ç»´åº¦é宿æ è¶å¿', left: 'center' }, |
| | | tooltip: { trigger: 'axis' }, |
| | | legend: { data: ['è®¢åæ°', 'éå®é¢', 'åè´§ç', '忬¾ç'], top: 30 }, |
| | | grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, |
| | | xAxis: { type: 'category', data: ['2024-12', '2025-01', '2025-02', '2025-03', '2025-04', '2025-05'] }, |
| | | yAxis: [ |
| | | { type: 'value', name: 'æ°é/éé¢', axisLabel: { formatter: '{value}' } }, |
| | | { type: 'value', name: 'æ¯ä¾(%)', min: 0, max: 100, axisLabel: { formatter: '{value}%' } } |
| | | ], |
| | | series: [ |
| | | { name: 'è®¢åæ°', type: 'bar', data: [180, 220, 210, 260, 205, 225], itemStyle: { color: '#409eff' } }, |
| | | { name: 'éå®é¢', type: 'bar', data: [820, 950, 910, 1080, 980, 1020], itemStyle: { color: '#67c23a' } }, |
| | | { name: 'åè´§ç', type: 'line', yAxisIndex: 1, data: [86, 89, 88, 91, 87, 90], itemStyle: { color: '#e6a23c' } }, |
| | | { name: '忬¾ç', type: 'line', yAxisIndex: 1, data: [72, 76, 74, 79, 75, 78], itemStyle: { color: '#f56c6c' } } |
| | | ] |
| | | } |
| | | indicatorChart.setOption(option) |
| | | } |
| | | |
| | | const applyIndicatorFilter = () => { |
| | | // 使ç¨åæ°æ®æ¨¡ææ¥è¯¢ï¼å·æ°KPIåå¾è¡¨ |
| | | // ä»
æ¼ç¤ºï¼éæºå¾®è°ä»¥ä½ç°çéææ |
| | | const random = (base, delta) => { |
| | | const v = base + Math.round((Math.random() - 0.5) * delta) |
| | | return v < 0 ? 0 : v |
| | | } |
| | | indicatorKpis.orderCount = random(1280, 120) |
| | | indicatorKpis.salesAmount = random(9650000, 350000) |
| | | indicatorKpis.shipmentRate = (85 + Math.random() * 10).toFixed(1) * 1 |
| | | indicatorKpis.collectionRate = (70 + Math.random() * 12).toFixed(1) * 1 |
| | | |
| | | setTimeout(() => initIndicatorChart(), 200) |
| | | } |
| | | |
| | | const resetIndicatorFilter = () => { |
| | | indicatorFilter.product = '' |
| | | indicatorFilter.customer = '' |
| | | indicatorFilter.region = '' |
| | | indicatorFilter.dateRange = [] |
| | | applyIndicatorFilter() |
| | | } |
| | | |
| | | const exportIndicatorTable = () => { |
| | | // 导åºå¢éä¸ç»©ä¸ºCSVï¼å导åºï¼ |
| | | const header = ['éå®å¢é', 'è®¢åæ°', 'éå®é¢', 'åè´§ç(%)', '忬¾ç(%)', 'ç®æ è¾¾æç(%)'] |
| | | const rows = teamPerformanceList.value.map(r => [ |
| | | r.team, |
| | | r.orderCount, |
| | | r.salesAmount, |
| | | r.shipmentRate, |
| | | r.collectionRate, |
| | | r.attainment |
| | | ]) |
| | | const csv = [header, ...rows].map(r => r.join(',')).join('\n') |
| | | const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }) |
| | | const url = URL.createObjectURL(blob) |
| | | const link = document.createElement('a') |
| | | link.href = url |
| | | link.download = 'ææ ç»è®¡-å¢éä¸ç»©.csv' |
| | | document.body.appendChild(link) |
| | | link.click() |
| | | document.body.removeChild(link) |
| | | URL.revokeObjectURL(url) |
| | | } |
| | | |
| | | const exportIndicatorChart = () => { |
| | | if (!indicatorChart) return |
| | | const url = indicatorChart.getDataURL({ type: 'png', pixelRatio: 2, backgroundColor: '#fff' }) |
| | | const link = document.createElement('a') |
| | | link.href = url |
| | | link.download = 'ææ ç»è®¡-å¾è¡¨.png' |
| | | document.body.appendChild(link) |
| | | link.click() |
| | | document.body.removeChild(link) |
| | | } |
| | | |
| | | // çå½å¨æ |
| | | onMounted(() => { |
| | | // ç»ä»¶æè½½åä¸ç«å³åå§åå¾è¡¨ï¼çå¾
ç¨æ·åæ¢å°å¯¹åºæ ç¾é¡µ |
| | |
| | | initPriceChart() |
| | | } else if (newVal === 'profitAnalysis') { |
| | | initProfitChart() |
| | | } else if (newVal === 'indicatorStats') { |
| | | initIndicatorChart() |
| | | } |
| | | }) |
| | | }) |