Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-management into dev_New
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, onBeforeUnmount, watchEffect } from 'vue' |
| | | import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue' |
| | | import * as echarts from 'echarts' |
| | | |
| | | const emit = defineEmits(['finished']) |
| | | |
| | | // Props |
| | | const props = defineProps({ |
| | |
| | | // Refs |
| | | const chartRef = ref(null) |
| | | let chartInstance = null |
| | | let finishedHandler = null |
| | | let initTimer = null |
| | | let initAttempts = 0 |
| | | |
| | | function clearInitTimer() { |
| | | if (initTimer) { |
| | | clearTimeout(initTimer) |
| | | initTimer = null |
| | | } |
| | | } |
| | | |
| | | function isContainerReady() { |
| | | const el = chartRef.value |
| | | if (!el) return false |
| | | // offsetWidth/offsetHeight æ´è´´è¿çå®å¸å±ï¼ä¸º 0 å¾å¾ä»£è¡¨è¿æ²¡å¸å±/ä¸å¯è§ï¼ |
| | | return el.offsetWidth > 0 && el.offsetHeight > 0 |
| | | } |
| | | |
| | | function initChartWhenReady() { |
| | | clearInitTimer() |
| | | initAttempts += 1 |
| | | |
| | | if (!isContainerReady()) { |
| | | // ç容å¨çæ£æå°ºå¯¸ï¼é¿å
é¦å±åå§ååç§»/空ç½ï¼çæ´æ°åææ£å¸¸çæ
åµï¼ |
| | | // æå¤éè¯çº¦ 3 ç§ï¼é¿å
æ éå¾ªç¯ |
| | | if (initAttempts < 60) { |
| | | initTimer = setTimeout(initChartWhenReady, 50) |
| | | } |
| | | return |
| | | } |
| | | |
| | | if (chartInstance) return |
| | | chartInstance = echarts.init(chartRef.value) |
| | | finishedHandler = () => emit('finished') |
| | | chartInstance.on('finished', finishedHandler) |
| | | renderChart() |
| | | // setOption åè¡¥ä¸æ¬¡ resizeï¼ç¡®ä¿é¦å±å°ºå¯¸æ£ç¡® |
| | | nextTick(() => { |
| | | if (chartInstance) chartInstance.resize() |
| | | }) |
| | | } |
| | | |
| | | // Methods |
| | | function generateChart(option) { |
| | |
| | | |
| | | // Lifecycle hooks |
| | | onMounted(() => { |
| | | chartInstance = echarts.init(chartRef.value) |
| | | renderChart() |
| | | initAttempts = 0 |
| | | initChartWhenReady() |
| | | window.addEventListener('resize', windowResizeListener) |
| | | }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | if (chartInstance) { |
| | | window.removeEventListener('resize', windowResizeListener) |
| | | if (finishedHandler) { |
| | | chartInstance.off('finished', finishedHandler) |
| | | finishedHandler = null |
| | | } |
| | | chartInstance.dispose() |
| | | chartInstance = null |
| | | } |
| | | clearInitTimer() |
| | | }) |
| | | |
| | | // Watch all reactive props that affect the chart |
| | | watch( |
| | | () => [props.xAxis, props.yAxis, props.series, props.legend, props.tooltip, props.visualMap], |
| | | () => { |
| | | if (chartInstance) { |
| | | renderChart() |
| | | // 妿é¦å±è¿æ²¡åå§åæåï¼çå¾
å®¹å¨ ready å忏²æ |
| | | if (!chartInstance) { |
| | | initChartWhenReady() |
| | | return |
| | | } |
| | | renderChart() |
| | | // æ°æ®åååè¡¥ä¸æ¬¡ resizeï¼é¿å
å¸å±åå导è´çåç§» |
| | | nextTick(() => { |
| | | if (chartInstance) chartInstance.resize() |
| | | }) |
| | | }, |
| | | { deep: true, immediate: true } |
| | | ) |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue' |
| | | |
| | | /** |
| | | * å¾è¡¨èæ¯ä½ç½®è°æ´ composable |
| | | * @param {Object} options é
ç½®é项 |
| | | * @param {Ref} options.wrapperRef - å¾è¡¨å®¹å¨ç ref |
| | | * @param {Ref} options.backgroundRef - èæ¯å
ç´ ç ref |
| | | * @param {String} options.left - èæ¯ left ä½ç½®ï¼å¦ '25%' æ '50%'ï¼é»è®¤ '50%' |
| | | * @param {String} options.top - èæ¯ top ä½ç½®ï¼å¦ '50%'ï¼é»è®¤ '50%' |
| | | * @param {String} options.offsetX - X è½´åç§»å¼ï¼å¦ '-51.5%' æ '-50%'ï¼é»è®¤ '-50%' |
| | | * @param {String} options.offsetY - Y è½´åç§»å¼ï¼å¦ '-39%' æ '-50%'ï¼é»è®¤ '-50%' |
| | | * @param {Ref} options.watchData - å¯éï¼çå¬çæ°æ®ååï¼æ°æ®ååæ¶éæ°è°æ´ä½ç½® |
| | | * @returns {Function} adjustBackgroundPosition - æå¨è°æ´èæ¯ä½ç½®çæ¹æ³ |
| | | */ |
| | | export function useChartBackground(options = {}) { |
| | | const { |
| | | wrapperRef, |
| | | backgroundRef, |
| | | left = '50%', |
| | | top = '50%', |
| | | offsetX = '-50%', |
| | | offsetY = '-50%', |
| | | watchData = null |
| | | } = options |
| | | |
| | | let resizeObserver = null |
| | | let intersectionObserver = null |
| | | let retryTimers = [] |
| | | |
| | | const clearRetryTimers = () => { |
| | | if (!retryTimers.length) return |
| | | retryTimers.forEach((t) => clearTimeout(t)) |
| | | retryTimers = [] |
| | | } |
| | | |
| | | // è°æ´èæ¯ä½ç½® |
| | | const adjustBackgroundPosition = () => { |
| | | nextTick(() => { |
| | | if (!wrapperRef?.value || !backgroundRef?.value) { |
| | | return |
| | | } |
| | | |
| | | // åå§åé¶æ®µç»å¸¸åºç°ï¼å®¹å¨å°æªå¯è§/尺寸为 0ï¼éå
¨å±ãtabãå¨ç»çï¼ |
| | | // è¿ç§æ
åµä¸å
ä¸å¯¹é½ï¼ç ResizeObserver / IntersectionObserver å触å |
| | | const rect = wrapperRef.value.getBoundingClientRect() |
| | | if (!rect.width || !rect.height) return |
| | | |
| | | const background = backgroundRef.value |
| | | |
| | | // 使ç¨ç¾åæ¯å®ä½ + transform å¾®è°ï¼è¿æ¯æå¯é çæ¹å¼ï¼ |
| | | background.style.left = left |
| | | background.style.top = top |
| | | background.style.transform = `translate(${offsetX}, ${offsetY})` |
| | | }) |
| | | } |
| | | |
| | | // åå§åé¶æ®µå¤æ¬¡âè¡¥å¿å¯¹é½âï¼è¦ç Echarts 馿¬¡æ¸²æ/å¨ç»é æçå»¶è¿å¸å± |
| | | const scheduleKickAlign = () => { |
| | | clearRetryTimers() |
| | | ;[0, 60, 180, 360, 800].forEach((ms) => { |
| | | retryTimers.push( |
| | | setTimeout(() => { |
| | | adjustBackgroundPosition() |
| | | }, ms) |
| | | ) |
| | | }) |
| | | } |
| | | |
| | | // çªå£ resize å¤ç |
| | | const resizeHandler = () => { |
| | | adjustBackgroundPosition() |
| | | } |
| | | |
| | | // 妿æä¾äº watchDataï¼ç嬿°æ®ååï¼éè¦å¨ setup é¶æ®µåå»ºï¼ |
| | | if (watchData) { |
| | | watch(watchData, () => { |
| | | adjustBackgroundPosition() |
| | | }, { deep: true }) |
| | | } |
| | | |
| | | // åå§å |
| | | const init = () => { |
| | | // çå¬çªå£ resize |
| | | window.addEventListener('resize', resizeHandler) |
| | | |
| | | // ä½¿ç¨ ResizeObserver çå¬å®¹å¨å°ºå¯¸åå |
| | | nextTick(() => { |
| | | if (wrapperRef?.value && window.ResizeObserver) { |
| | | resizeObserver = new ResizeObserver(() => { |
| | | adjustBackgroundPosition() |
| | | }) |
| | | resizeObserver.observe(wrapperRef.value) |
| | | } |
| | | |
| | | // çå¬âä»ä¸å¯è§å°å¯è§âï¼è§£å³åå§åæ¶æªå¯¹é½ä½çæ´æ°åæ£å¸¸çé®é¢ |
| | | if (wrapperRef?.value && window.IntersectionObserver) { |
| | | intersectionObserver = new IntersectionObserver( |
| | | (entries) => { |
| | | const entry = entries?.[0] |
| | | if (entry?.isIntersecting) { |
| | | scheduleKickAlign() |
| | | } |
| | | }, |
| | | { threshold: 0.01 } |
| | | ) |
| | | intersectionObserver.observe(wrapperRef.value) |
| | | } |
| | | |
| | | // åå§å夿¬¡è¡¥å¿å¯¹é½ï¼ç¡®ä¿å¾è¡¨æ¸²æå®æ |
| | | scheduleKickAlign() |
| | | }) |
| | | } |
| | | |
| | | // æ¸
ç |
| | | const cleanup = () => { |
| | | window.removeEventListener('resize', resizeHandler) |
| | | clearRetryTimers() |
| | | if (resizeObserver) { |
| | | resizeObserver.disconnect() |
| | | resizeObserver = null |
| | | } |
| | | if (intersectionObserver) { |
| | | intersectionObserver.disconnect() |
| | | intersectionObserver = null |
| | | } |
| | | } |
| | | |
| | | return { |
| | | adjustBackgroundPosition, |
| | | init, |
| | | cleanup |
| | | } |
| | | } |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æææï¼" prop="valid"> |
| | | <el-form-item label="æææ¥æ(天)ï¼" prop="valid"> |
| | | <el-input |
| | | v-model="form.valid" |
| | | placeholder="请è¾å
¥" |
| | | type="number" |
| | | placeholder="请è¾å
¥æææå¤©æ°" |
| | | clearable |
| | | :min="1" |
| | | @input="handleValidInput" |
| | | > |
| | | <template #append>æ¥</template> |
| | | </el-input> |
| | |
| | | rules: { |
| | | code: [{required: true, message: "请è¾å
¥", trigger: "blur"}], |
| | | name: [{required: true, message: "请è¾å
¥", trigger: "blur"}], |
| | | valid: [{required: true, message: "请è¾å
¥", trigger: "blur"}], |
| | | valid: [ |
| | | {required: true, message: "请è¾å
¥", trigger: "blur"}, |
| | | { |
| | | validator: (rule, value, callback) => { |
| | | if (value === '' || value === null || value === undefined) { |
| | | callback(); |
| | | return; |
| | | } |
| | | const numValue = Number(value); |
| | | if (isNaN(numValue)) { |
| | | callback(new Error('请è¾å
¥ææçæ°å')); |
| | | return; |
| | | } |
| | | if (numValue <= 0) { |
| | | callback(new Error('åªè½è¾å
¥æ£æ°')); |
| | | return; |
| | | } |
| | | if (!Number.isInteger(numValue)) { |
| | | callback(new Error('请è¾å
¥æ´æ°')); |
| | | return; |
| | | } |
| | | callback(); |
| | | }, |
| | | trigger: 'blur' |
| | | } |
| | | ], |
| | | recordDate: [{required: true, message: "è¯·éæ©", trigger: "change"}], |
| | | userId: [{required: true, message: "è¯·éæ©", trigger: "change"}], |
| | | entryDate: [{required: true, message: "è¯·éæ©", trigger: "change"}], |
| | |
| | | } |
| | | } |
| | | |
| | | // å¤çæææ¥æè¾å
¥ï¼åªå
è®¸æ£æ´æ° |
| | | const handleValidInput = (value) => { |
| | | if (value === '' || value === null || value === undefined) { |
| | | form.value.valid = ''; |
| | | return; |
| | | } |
| | | // 转æ¢ä¸ºåç¬¦ä¸²å¹¶ç§»é¤ææéæ°åå符ï¼å
æ¬è´å·ãå°æ°ç¹çï¼ |
| | | const numStr = String(value).replace(/[^0-9]/g, ''); |
| | | if (numStr === '') { |
| | | form.value.valid = ''; |
| | | return; |
| | | } |
| | | const numValue = parseInt(numStr, 10); |
| | | // ç¡®ä¿æ¯æ£æ´æ°ï¼å¤§äº0ï¼ |
| | | if (numValue > 0 && !isNaN(numValue)) { |
| | | form.value.valid = numValue; |
| | | } else { |
| | | form.value.valid = ''; |
| | | } |
| | | } |
| | | |
| | | const submitForm = () => { |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | |
| | | ref="formRef" |
| | | > |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åºåç¼å·ï¼" prop="code"> |
| | | <el-input |
| | | v-model="form.code" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="计éå¨å
·åç§°ï¼" prop="name"> |
| | | <el-input |
| | | v-model="form.name" |
| | | placeholder="请è¾å
¥è®¡éå¨å
·åç§°" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | |
| | | <el-form-item label="æææ¥æ(天)ï¼" prop="valid"> |
| | | <el-input |
| | | v-model="form.valid" |
| | | type="number" |
| | | placeholder="请è¾å
¥æææå¤©æ°" |
| | | clearable |
| | | :min="1" |
| | | @input="handleValidInput" |
| | | > |
| | | <template #append>æ¥</template> |
| | | </el-input> |
| | |
| | | const data = reactive({ |
| | | form: { |
| | | code: "", |
| | | name: "", |
| | | instationLocation: "", |
| | | mostDate:"", |
| | | model: "", |
| | |
| | | }, |
| | | rules: { |
| | | code: [{required: true, message: "请è¾å
¥", trigger: "blur"}], |
| | | name: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | model: [{required: true, message: "请è¾å
¥", trigger: "blur"}], |
| | | validDate: [{required: true, message: "请è¾å
¥", trigger: "blur"}], |
| | | nextDate: [{required: true, message: "è¯·éæ©", trigger: "change"}], |
| | |
| | | instationLocation: [{required: true, message: "请è¾å
¥", trigger: "blur"}], |
| | | mostDate: [{required: true, message: "è¯·éæ©", trigger: "change"}], |
| | | cycle: [{required: true, message: "è¯·éæ©", trigger: "blur"}], |
| | | valid: [{required: true, message: "请è¾å
¥", trigger: "blur"}], |
| | | valid: [ |
| | | {required: true, message: "请è¾å
¥", trigger: "blur"}, |
| | | { |
| | | validator: (rule, value, callback) => { |
| | | if (value === '' || value === null || value === undefined) { |
| | | callback(); |
| | | return; |
| | | } |
| | | const numValue = Number(value); |
| | | if (isNaN(numValue)) { |
| | | callback(new Error('请è¾å
¥ææçæ°å')); |
| | | return; |
| | | } |
| | | if (numValue <= 0) { |
| | | callback(new Error('åªè½è¾å
¥æ£æ°')); |
| | | return; |
| | | } |
| | | if (!Number.isInteger(numValue)) { |
| | | callback(new Error('请è¾å
¥æ´æ°')); |
| | | return; |
| | | } |
| | | callback(); |
| | | }, |
| | | trigger: 'blur' |
| | | } |
| | | ], |
| | | unit: [{required: true, message: "请è¾å
¥", trigger: "blur"}], |
| | | } |
| | | }) |
| | |
| | | } |
| | | } |
| | | |
| | | // å¤çæææ¥æè¾å
¥ï¼åªå
è®¸æ£æ´æ° |
| | | const handleValidInput = (value) => { |
| | | if (value === '' || value === null || value === undefined) { |
| | | form.value.valid = ''; |
| | | return; |
| | | } |
| | | // 转æ¢ä¸ºåç¬¦ä¸²å¹¶ç§»é¤ææéæ°åå符ï¼å
æ¬è´å·ãå°æ°ç¹çï¼ |
| | | const numStr = String(value).replace(/[^0-9]/g, ''); |
| | | if (numStr === '') { |
| | | form.value.valid = ''; |
| | | return; |
| | | } |
| | | const numValue = parseInt(numStr, 10); |
| | | // ç¡®ä¿æ¯æ£æ´æ°ï¼å¤§äº0ï¼ |
| | | if (numValue > 0 && !isNaN(numValue)) { |
| | | form.value.valid = numValue; |
| | | } else { |
| | | form.value.valid = ''; |
| | | } |
| | | } |
| | | |
| | | const submitForm = () => { |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | |
| | | minWidth:150, |
| | | align:"center" |
| | | }, |
| | | { |
| | | label: "计éå¨å
·åç§°", |
| | | prop: "name", |
| | | width: '160px', |
| | | align: "center", |
| | | }, |
| | | { |
| | | label: "å®è£
ä½ç½®", |
| | | prop: "instationLocation", |
| | |
| | | </el-table-column> |
| | | <el-table-column prop="price" label="ä»·æ ¼" width="140"></el-table-column> |
| | | <el-table-column prop="quantity" label="æ°é" width="140"></el-table-column> |
| | | <el-table-column prop="description" label="æè¿°" width="150"></el-table-column> |
| | | <el-table-column prop="description" label="æè¿°"></el-table-column> |
| | | <el-table-column label="æä½" width="150" fixed="right" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-button |
| | |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <!-- å页ç»ä»¶ --> |
| | | <div class="pagination-container"> |
| | | <el-pagination |
| | | v-model:current-page="pagination.current" |
| | | v-model:page-size="pagination.size" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | :total="pagination.total" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | @size-change="handleSizeChange" |
| | | @current-change="handleCurrentChange" |
| | | /> |
| | | </div> |
| | | </div> |
| | | <el-dialog title="å类管ç" v-model="dialogVisible" width="60%"> |
| | | <el-form :model="form" :rules="rules" ref="formRef" label-width="100px"> |
| | |
| | | const queryParams = reactive({ |
| | | name: '' |
| | | }); |
| | | // å页忰 |
| | | const pagination = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0 |
| | | }); |
| | | // è¡¨åæ°æ® |
| | | const form = reactive({ |
| | | id:'', |
| | |
| | | const fetchListData = async () => { |
| | | loading.value = true; |
| | | try { |
| | | const params = {}; |
| | | const params = { |
| | | current: pagination.current, |
| | | size: pagination.size |
| | | }; |
| | | if (queryParams.name) { |
| | | params.name = queryParams.name; |
| | | } |
| | |
| | | if (res.code === 200) { |
| | | renderTableData.value = res.data.records || []; |
| | | categories.value = res.data.records || []; |
| | | pagination.total = res.data.total || 0; |
| | | } |
| | | } catch (error) { |
| | | loading.value = false; |
| | |
| | | |
| | | // æ¥è¯¢ |
| | | const handleQuery = () => { |
| | | pagination.current = 1; |
| | | fetchListData(); |
| | | } |
| | | |
| | | // éç½®æ¥è¯¢ |
| | | const resetQuery = () => { |
| | | queryParams.name = ''; |
| | | pagination.current = 1; |
| | | fetchListData(); |
| | | } |
| | | |
| | | // å页大尿¹å |
| | | const handleSizeChange = (size) => { |
| | | pagination.size = size; |
| | | pagination.current = 1; |
| | | fetchListData(); |
| | | } |
| | | |
| | | // å½å页æ¹å |
| | | const handleCurrentChange = (current) => { |
| | | pagination.current = current; |
| | | fetchListData(); |
| | | } |
| | | |
| | |
| | | margin-top: unset; |
| | | } |
| | | |
| | | .pagination-container { |
| | | margin-top: 20px; |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | padding: 16px 0; |
| | | } |
| | | |
| | | .el-table__header-wrapper th { |
| | | background-color: #f5f7fa; |
| | | font-weight: 600; |
| | |
| | | <PanelHeader title="éè´ååå¸" /> |
| | | <div class="main-panel panel-item-customers"> |
| | | <CarouselCards :items="cardItems" :visible-count="3" /> |
| | | <div class="pie-chart-wrapper"> |
| | | <div class="pie-background"></div> |
| | | <div class="pie-chart-wrapper" ref="pieWrapperRef"> |
| | | <div class="pie-background" ref="pieBackgroundRef"></div> |
| | | <Echarts |
| | | ref="chart" |
| | | :chartStyle="chartStyle" |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, computed } from 'vue' |
| | | import { ref, onMounted, onBeforeUnmount, computed } from 'vue' |
| | | import Echarts from '@/components/Echarts/echarts.vue' |
| | | import PanelHeader from './PanelHeader.vue' |
| | | import CarouselCards from './CarouselCards.vue' |
| | | import { rawMaterialPurchaseAmountRatio } from '@/api/viewIndex.js' |
| | | import { useChartBackground } from '@/hooks/useChartBackground.js' |
| | | |
| | | const pieWrapperRef = ref(null) |
| | | const pieBackgroundRef = ref(null) |
| | | |
| | | /** |
| | | * @introduction ææ°ç»ä¸keyå¼ç¸åçé£ä¸é¡¹æååºæ¥ï¼ç»æä¸ä¸ªå¯¹è±¡ |
| | |
| | | textStyle: { color: '#B8C8E0' }, |
| | | } |
| | | |
| | | // 使ç¨å°è£
çèæ¯ä½ç½®è°æ´æ¹æ³ |
| | | // å¾è¡¨ä¸å¿æ¯ ['25%', '50%']ï¼èæ¯éè¦å¯¹é½å°è¿ä¸ªä½ç½® |
| | | const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({ |
| | | wrapperRef: pieWrapperRef, |
| | | backgroundRef: pieBackgroundRef, |
| | | left: '25%', // å¾è¡¨ä¸å¿ X æ¯ 25% |
| | | top: '50%', // å¾è¡¨ä¸å¿ Y æ¯ 50% |
| | | offsetX: '-51.5%', // X è½´åç§» |
| | | offsetY: '-50%', // Y è½´åç§» |
| | | watchData: dataList // ç嬿°æ®ååï¼èªå¨è°æ´ä½ç½® |
| | | }) |
| | | |
| | | const fetchData = () => { |
| | | rawMaterialPurchaseAmountRatio() |
| | | .then((res) => { |
| | |
| | | |
| | | onMounted(() => { |
| | | fetchData() |
| | | initBackground() |
| | | }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | cleanupBackground() |
| | | }) |
| | | </script> |
| | | |
| | |
| | | |
| | | .pie-background { |
| | | position: absolute; |
| | | left: 25%; |
| | | top: 50%; |
| | | transform: translate(-51.5%, -50%); |
| | | width: 310px; |
| | | height: 310px; |
| | | background-image: url('@/assets/BI/ç«ç°å¾è¾¹æ¡.png'); |
| | |
| | | background-repeat: no-repeat; |
| | | z-index: 1; |
| | | pointer-events: none; |
| | | /* ä½ç½®ç± JS å¨æè®¾ç½®ï¼é»è®¤å±
ä¸ */ |
| | | left: 25%; |
| | | top: 50%; |
| | | transform: translate(-51.5%, -50%); |
| | | } |
| | | </style> |
| | |
| | | <PanelHeader title="éå®ååå¸" /> |
| | | <div class="main-panel panel-item-customers"> |
| | | <CarouselCards :items="cardItems" :visible-count="3" /> |
| | | <div class="pie-chart-wrapper"> |
| | | <div class="pie-background"></div> |
| | | <div class="pie-chart-wrapper" ref="pieWrapperRef"> |
| | | <div class="pie-background" ref="pieBackgroundRef"></div> |
| | | <Echarts |
| | | ref="echartsRef" |
| | | :chartStyle="chartStyle" |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, computed } from 'vue' |
| | | import { ref, onMounted, onBeforeUnmount, computed } from 'vue' |
| | | import { productSalesAnalysis } from '@/api/viewIndex.js' |
| | | import PanelHeader from './PanelHeader.vue' |
| | | import CarouselCards from './CarouselCards.vue' |
| | | import Echarts from '@/components/Echarts/echarts.vue' |
| | | import { useChartBackground } from '@/hooks/useChartBackground.js' |
| | | |
| | | const pieWrapperRef = ref(null) |
| | | const pieBackgroundRef = ref(null) |
| | | |
| | | /** |
| | | * @introduction ææ°ç»ä¸keyå¼ç¸åçé£ä¸é¡¹æååºæ¥ï¼ç»æä¸ä¸ªå¯¹è±¡ |
| | |
| | | |
| | | const cardItems = ref([]) |
| | | |
| | | // 使ç¨å°è£
çèæ¯ä½ç½®è°æ´æ¹æ³ï¼ä¸å
¶ä»æä»¶ä¿æä¸è´ï¼ |
| | | const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({ |
| | | wrapperRef: pieWrapperRef, |
| | | backgroundRef: pieBackgroundRef, |
| | | left: '25%', // å¾è¡¨ä¸å¿ X æ¯ 25% |
| | | top: '50%', // å¾è¡¨ä¸å¿ Y æ¯ 50% |
| | | offsetX: '-51.5%', // X è½´åç§» |
| | | offsetY: '-50%', // Y è½´åç§» |
| | | watchData: pieDatas // ç嬿°æ®ååï¼èªå¨è°æ´ä½ç½® |
| | | }) |
| | | |
| | | const fetchData = () => { |
| | | productSalesAnalysis() |
| | | .then((res) => { |
| | |
| | | |
| | | onMounted(() => { |
| | | fetchData() |
| | | initBackground() |
| | | }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | cleanupBackground() |
| | | }) |
| | | </script> |
| | | |
| | |
| | | <div> |
| | | <PanelHeader title="产å大类" /> |
| | | <div class="panel-item-customers"> |
| | | <div class="pie-chart-wrapper"> |
| | | <div class="pie-background"></div> |
| | | <div class="pie-chart-wrapper" ref="pieWrapperRef"> |
| | | <div class="pie-background" ref="pieBackgroundRef"></div> |
| | | <Echarts |
| | | ref="chart" |
| | | :chartStyle="chartStyle" |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted } from 'vue' |
| | | import { ref, onMounted, onBeforeUnmount } from 'vue' |
| | | import Echarts from '@/components/Echarts/echarts.vue' |
| | | import PanelHeader from '../PanelHeader.vue' |
| | | import { productCategoryDistribution } from '@/api/viewIndex.js' |
| | | import { useChartBackground } from '@/hooks/useChartBackground.js' |
| | | |
| | | const pieWrapperRef = ref(null) |
| | | const pieBackgroundRef = ref(null) |
| | | const chart = ref(null) |
| | | |
| | | // æ°æ®åè¡¨ï¼æ¥èªæ¥å£ï¼ |
| | | const dataList = ref([]) |
| | |
| | | textStyle: { color: '#B8C8E0' }, |
| | | } |
| | | |
| | | // 使ç¨å°è£
çèæ¯ä½ç½®è°æ´æ¹æ³ï¼å¯èªå®ä¹åç§»å¼ |
| | | const { adjustBackgroundPosition, init: initBackground, cleanup: cleanupBackground } = useChartBackground({ |
| | | wrapperRef: pieWrapperRef, |
| | | backgroundRef: pieBackgroundRef, |
| | | offsetX: '-51.5%', // X è½´åç§»ï¼å¯å¨æè°æ´ |
| | | offsetY: '-39%', // Y è½´åç§»ï¼å¯å¨æè°æ´ |
| | | watchData: dataList // ç嬿°æ®ååï¼èªå¨è°æ´ä½ç½® |
| | | }) |
| | | |
| | | const loadData = async () => { |
| | | try { |
| | | const res = await productCategoryDistribution() |
| | |
| | | })) |
| | | landLegend.data = dataList.value.map((d) => d.name) |
| | | landSeries.value[0].data = dataList.value |
| | | // æ°æ®å è½½å®æåè°æ´èæ¯ä½ç½® |
| | | adjustBackgroundPosition() |
| | | } catch (e) { |
| | | console.error('è·å产å大类åå¸å¤±è´¥:', e) |
| | | dataList.value = [] |
| | |
| | | } |
| | | } |
| | | |
| | | |
| | | onMounted(() => { |
| | | loadData() |
| | | initBackground() |
| | | }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | cleanupBackground() |
| | | }) |
| | | </script> |
| | | |
| | |
| | | |
| | | .pie-background { |
| | | position: absolute; |
| | | left: 50%; |
| | | top: 50%; |
| | | transform: translate(-51.5%, -39%); |
| | | width: 360px; |
| | | height: 360px; |
| | | background-image: url('@/assets/BI/ç«ç°å¾è¾¹æ¡.png'); |
| | |
| | | background-repeat: no-repeat; |
| | | z-index: 1; |
| | | pointer-events: none; |
| | | /* é»è®¤å±
ä¸ï¼ä¼å¨ JS ä¸å¨æè°æ´ */ |
| | | left: 50%; |
| | | top: 50%; |
| | | transform: translate(-51.5%, -39%); |
| | | } |
| | | </style> |
| | |
| | | /> |
| | | </div> |
| | | <!-- <CarouselCards :items="cardItems" :visible-count="3" /> --> |
| | | <div class="pie-chart-wrapper"> |
| | | <div class="pie-background"></div> |
| | | <div class="pie-chart-wrapper" ref="pieWrapperRef"> |
| | | <div class="pie-background" ref="pieBackgroundRef"></div> |
| | | <Echarts |
| | | ref="chart" |
| | | :chartStyle="chartStyle" |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, computed } from 'vue' |
| | | import { ref, onMounted, onBeforeUnmount, computed } from 'vue' |
| | | import Echarts from '@/components/Echarts/echarts.vue' |
| | | import PanelHeader from './PanelHeader.vue' |
| | | import ProductTypeSwitch from './ProductTypeSwitch.vue' |
| | | import { expenseCompositionAnalysis } from '@/api/viewIndex.js' |
| | | import { useChartBackground } from '@/hooks/useChartBackground.js' |
| | | |
| | | const pieWrapperRef = ref(null) |
| | | const pieBackgroundRef = ref(null) |
| | | |
| | | /** |
| | | * @introduction ææ°ç»ä¸keyå¼ç¸åçé£ä¸é¡¹æååºæ¥ï¼ç»æä¸ä¸ªå¯¹è±¡ |
| | |
| | | textStyle: { color: '#B8C8E0' }, |
| | | } |
| | | |
| | | // 使ç¨å°è£
çèæ¯ä½ç½®è°æ´æ¹æ³ |
| | | // å¾è¡¨ä¸å¿æ¯ ['25%', '50%']ï¼èæ¯éè¦å¯¹é½å°è¿ä¸ªä½ç½® |
| | | const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({ |
| | | wrapperRef: pieWrapperRef, |
| | | backgroundRef: pieBackgroundRef, |
| | | left: '25%', // å¾è¡¨ä¸å¿ X æ¯ 25% |
| | | top: '50%', // å¾è¡¨ä¸å¿ Y æ¯ 50% |
| | | offsetX: '-51.5%', // X è½´åç§» |
| | | offsetY: '-50%', // Y è½´åç§» |
| | | watchData: dataList // ç嬿°æ®ååï¼èªå¨è°æ´ä½ç½® |
| | | }) |
| | | |
| | | const fetchData = () => { |
| | | expenseCompositionAnalysis({ type: amountType.value }) |
| | | .then((res) => { |
| | |
| | | |
| | | onMounted(() => { |
| | | fetchData() |
| | | initBackground() |
| | | }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | cleanupBackground() |
| | | }) |
| | | </script> |
| | | |
| | |
| | | |
| | | .pie-background { |
| | | position: absolute; |
| | | left: 25%; |
| | | top: 50%; |
| | | transform: translate(-51.5%, -50%); |
| | | width: 310px; |
| | | height: 310px; |
| | | background-image: url('@/assets/BI/ç«ç°å¾è¾¹æ¡.png'); |
| | |
| | | background-repeat: no-repeat; |
| | | z-index: 1; |
| | | pointer-events: none; |
| | | /* ä½ç½®ç± JS å¨æè®¾ç½®ï¼é»è®¤å±
ä¸ */ |
| | | left: 25%; |
| | | top: 50%; |
| | | transform: translate(-51.5%, -50%); |
| | | } |
| | | </style> |
| | |
| | | <div class="filters-row"> |
| | | <DateTypeSwitch v-model="dateType" @change="handleDateTypeChange" /> |
| | | </div> |
| | | <div class="pie-chart-wrapper"> |
| | | <div class="pie-background"></div> |
| | | <div class="pie-chart-wrapper" ref="pieWrapperRef"> |
| | | <div class="pie-background" ref="pieBackgroundRef"></div> |
| | | <Echarts |
| | | ref="echartsRef" |
| | | :chartStyle="chartStyle" |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, computed } from 'vue' |
| | | import { ref, onMounted, onBeforeUnmount, computed } from 'vue' |
| | | import { productSalesAnalysis } from '@/api/viewIndex.js' |
| | | import PanelHeader from './PanelHeader.vue' |
| | | import Echarts from '@/components/Echarts/echarts.vue' |
| | | import DateTypeSwitch from '@/views/reportAnalysis/financialAnalysis/components/DateTypeSwitch.vue' |
| | | import { useChartBackground } from '@/hooks/useChartBackground.js' |
| | | |
| | | const pieWrapperRef = ref(null) |
| | | const pieBackgroundRef = ref(null) |
| | | |
| | | const dateType = ref(1) // 1=å¨ 2=æ 3=å£åº¦ |
| | | |
| | |
| | | textStyle: { color: '#B8C8E0' }, |
| | | } |
| | | |
| | | // 使ç¨å°è£
çèæ¯ä½ç½®è°æ´æ¹æ³ |
| | | // å¾è¡¨ä¸å¿æ¯ ['25%', '50%']ï¼èæ¯éè¦å¯¹é½å°è¿ä¸ªä½ç½® |
| | | const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({ |
| | | wrapperRef: pieWrapperRef, |
| | | backgroundRef: pieBackgroundRef, |
| | | left: '25%', // å¾è¡¨ä¸å¿ X æ¯ 25% |
| | | top: '50%', // å¾è¡¨ä¸å¿ Y æ¯ 50% |
| | | offsetX: '-51.5%', // X è½´åç§» |
| | | offsetY: '-50%', // Y è½´åç§» |
| | | watchData: pieDatas // ç嬿°æ®ååï¼èªå¨è°æ´ä½ç½® |
| | | }) |
| | | |
| | | const fetchData = () => { |
| | | productSalesAnalysis() |
| | | .then((res) => { |
| | |
| | | |
| | | onMounted(() => { |
| | | fetchData() |
| | | initBackground() |
| | | }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | cleanupBackground() |
| | | }) |
| | | </script> |
| | | |
| | |
| | | |
| | | .pie-background { |
| | | position: absolute; |
| | | left: 25%; |
| | | top: 50%; |
| | | transform: translate(-51.5%, -50%); |
| | | width: 310px; |
| | | height: 310px; |
| | | background-image: url('@/assets/BI/ç«ç°å¾è¾¹æ¡.png'); |
| | |
| | | background-repeat: no-repeat; |
| | | z-index: 1; |
| | | pointer-events: none; |
| | | /* ä½ç½®ç± JS å¨æè®¾ç½®ï¼é»è®¤å±
ä¸ */ |
| | | left: 25%; |
| | | top: 50%; |
| | | transform: translate(-51.5%, -50%); |
| | | } |
| | | </style> |
| | |
| | | <div class="app-container"> |
| | | <div class="search_form"> |
| | | <div> |
| | | <span class="search_title">课ç¨ç¼å·ï¼</span> |
| | | <el-input v-model="searchForm.courseCode" |
| | | style="width: 240px" |
| | | placeholder="请è¾å
¥å¹è®ç¼å·æç´¢" |
| | | @change="handleQuery" |
| | | clearable |
| | | :prefix-icon="Search" /> |
| | | <span class="search_title ml10">å¹è®æ¹å¼ï¼</span> |
| | | <el-select v-model="searchForm.trainingMode" |
| | | clearable |
| | | @change="handleQuery" |
| | | style="width: 240px"> |
| | | <el-option v-for="item in trainingModeOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" /> |
| | | </el-select> |
| | | <span class="search_title">å¹è®æ¥æï¼</span> |
| | | <el-date-picker v-model="searchForm.trainingDate" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | @change="handleQuery" |
| | | type="date" |
| | | placeholder="è¯·éæ©" |
| | | clearable /> |
| | | <el-button type="primary" |
| | | @click="handleQuery" |
| | | style="margin-left: 10px"> |
| | |
| | | // ååºå¼æ°æ® |
| | | const data = reactive({ |
| | | searchForm: { |
| | | courseCode: "", |
| | | trainingMode: "", |
| | | trainingDate: "", |
| | | state: 0, |
| | | }, |
| | | tableLoading: false, |