Compare commits
No commits in common. 'main' and 'liutao_branch' have entirely different histories.
main
...
liutao_bra
@ -0,0 +1,144 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"npm.packageManager": "pnpm",
|
||||
"editor.tabSize": 2,
|
||||
"prettier.printWidth": 100, // 超过最大值换行
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"files.eol": "\n",
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/*.log": true,
|
||||
"**/*.log*": true,
|
||||
"**/bower_components": true,
|
||||
"**/dist": true,
|
||||
"**/elehukouben": true,
|
||||
"**/.git": true,
|
||||
"**/.gitignore": true,
|
||||
"**/.svn": true,
|
||||
"**/.DS_Store": true,
|
||||
"**/.idea": true,
|
||||
"**/.vscode": false,
|
||||
"**/yarn.lock": true,
|
||||
"**/tmp": true,
|
||||
"out": true,
|
||||
"dist": true,
|
||||
"node_modules": true,
|
||||
"CHANGELOG.md": true,
|
||||
"examples": true,
|
||||
"res": true,
|
||||
"screenshots": true,
|
||||
"yarn-error.log": true,
|
||||
"**/.yarn": true
|
||||
},
|
||||
"files.exclude": {
|
||||
"**/.cache": true,
|
||||
"**/.editorconfig": true,
|
||||
"**/.eslintcache": true,
|
||||
"**/bower_components": true,
|
||||
"**/.idea": true,
|
||||
"**/tmp": true,
|
||||
"**/.git": true,
|
||||
"**/.svn": true,
|
||||
"**/.hg": true,
|
||||
"**/CVS": true,
|
||||
"**/.DS_Store": true
|
||||
},
|
||||
"files.watcherExclude": {
|
||||
"**/.git/objects/**": true,
|
||||
"**/.git/subtree-cache/**": true,
|
||||
"**/.vscode/**": true,
|
||||
"**/node_modules/**": true,
|
||||
"**/tmp/**": true,
|
||||
"**/bower_components/**": true,
|
||||
"**/dist/**": true,
|
||||
"**/yarn.lock": true
|
||||
},
|
||||
"stylelint.enable": true,
|
||||
"stylelint.validate": ["css", "less", "postcss", "scss", "vue", "sass"],
|
||||
"path-intellisense.mappings": {
|
||||
"@/": "${workspaceRoot}/src"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
||||
},
|
||||
"[less]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[scss]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "Vue.volar"
|
||||
},
|
||||
"i18n-ally.localesPaths": ["src/locales"],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.sortKeys": true,
|
||||
"i18n-ally.namespace": false,
|
||||
"i18n-ally.enabledParsers": ["ts"],
|
||||
"i18n-ally.sourceLanguage": "en",
|
||||
"i18n-ally.displayLanguage": "zh-CN",
|
||||
"i18n-ally.enabledFrameworks": ["vue", "react"],
|
||||
"cSpell.words": [
|
||||
"brotli",
|
||||
"browserslist",
|
||||
"codemirror",
|
||||
"commitlint",
|
||||
"cropperjs",
|
||||
"echart",
|
||||
"echarts",
|
||||
"esnext",
|
||||
"esno",
|
||||
"iconify",
|
||||
"INTLIFY",
|
||||
"lintstagedrc",
|
||||
"logicflow",
|
||||
"nprogress",
|
||||
"pinia",
|
||||
"pnpm",
|
||||
"qrcode",
|
||||
"sider",
|
||||
"sortablejs",
|
||||
"stylelint",
|
||||
"svgs",
|
||||
"unocss",
|
||||
"unplugin",
|
||||
"unref",
|
||||
"videojs",
|
||||
"VITE",
|
||||
"vitejs",
|
||||
"vueuse",
|
||||
"wangeditor",
|
||||
"xingyu",
|
||||
"yudao",
|
||||
"zxcvbn"
|
||||
],
|
||||
// 控制相关文件嵌套展示
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.expand": false,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"*.ts": "$(capture).test.ts, $(capture).test.tsx",
|
||||
"*.tsx": "$(capture).test.ts, $(capture).test.tsx",
|
||||
"*.env": "$(capture).env.*",
|
||||
"package.json": "pnpm-lock.yaml,yarn.lock,LICENSE,README*,CHANGELOG*,CNAME,.gitattributes,.eslintrc-auto-import.json,.gitignore,prettier.config.js,stylelint.config.js,commitlint.config.js,.stylelintignore,.prettierignore,.gitpod.yml,.eslintrc.js,.eslintignore"
|
||||
},
|
||||
"terminal.integrated.scrollback": 10000,
|
||||
"nuxt.isNuxtApp": false
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export interface WarehouseAreaVO {
|
||||
id: number
|
||||
warehouseId: number
|
||||
areaCode: string
|
||||
areaName: string
|
||||
areaSize: number
|
||||
description: string
|
||||
status: number
|
||||
createTime: string
|
||||
}
|
||||
|
||||
export const WarehouseAreaApi = {
|
||||
getWarehouseAreaPage: async (params: any) => {
|
||||
return await request.get({ url: `/erp/warehouse-area/page`, params })
|
||||
},
|
||||
|
||||
getWarehouseArea: async (id: number) => {
|
||||
return await request.get({ url: `/erp/warehouse-area/get?id=` + id })
|
||||
},
|
||||
|
||||
createWarehouseArea: async (data: WarehouseAreaVO) => {
|
||||
return await request.post({ url: `/erp/warehouse-area/create`, data })
|
||||
},
|
||||
|
||||
updateWarehouseArea: async (data: WarehouseAreaVO) => {
|
||||
return await request.put({ url: `/erp/warehouse-area/update`, data })
|
||||
},
|
||||
|
||||
deleteWarehouseArea: async (id: number) => {
|
||||
return await request.delete({ url: `/erp/warehouse-area/delete?id=` + id })
|
||||
}
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export interface WarehouseLocationVO {
|
||||
id: number
|
||||
warehouseId: number
|
||||
areaId: number
|
||||
code: string
|
||||
name: string
|
||||
areaSize: number
|
||||
maxLoadWeight: number
|
||||
positionX: number
|
||||
positionY: number
|
||||
positionZ: number
|
||||
allowProductMix: boolean
|
||||
allowBatchMix: boolean
|
||||
status: number
|
||||
createTime: string
|
||||
}
|
||||
|
||||
export const WarehouseLocationApi = {
|
||||
getWarehouseLocationPage: async (params: any) => {
|
||||
return await request.get({ url: `/erp/warehouse-location/page`, params })
|
||||
},
|
||||
|
||||
getWarehouseLocation: async (id: number) => {
|
||||
return await request.get({ url: `/erp/warehouse-location/get?id=` + id })
|
||||
},
|
||||
|
||||
createWarehouseLocation: async (data: WarehouseLocationVO) => {
|
||||
return await request.post({ url: `/erp/warehouse-location/create`, data })
|
||||
},
|
||||
|
||||
updateWarehouseLocation: async (data: WarehouseLocationVO) => {
|
||||
return await request.put({ url: `/erp/warehouse-location/update`, data })
|
||||
},
|
||||
|
||||
deleteWarehouseLocation: async (id: number) => {
|
||||
return await request.delete({ url: `/erp/warehouse-location/delete?id=` + id })
|
||||
}
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export interface DeviceOperationOverviewParams {
|
||||
ids?: string
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
timelinePageNo?: number
|
||||
timelinePageSize?: number
|
||||
}
|
||||
|
||||
export interface DeviceOperationOverviewMetricVO {
|
||||
key: string
|
||||
icon: string
|
||||
value: number
|
||||
unit: string
|
||||
change: number
|
||||
}
|
||||
|
||||
export interface DeviceOperationOverviewHourlyStatusVO {
|
||||
hour: string
|
||||
running: number
|
||||
standby: number
|
||||
fault: number
|
||||
offline: number
|
||||
}
|
||||
|
||||
export interface DeviceOperationOverviewSummaryVO {
|
||||
status: 'running' | 'standby' | 'fault' | 'offline'
|
||||
percent: number
|
||||
hours: number
|
||||
}
|
||||
|
||||
export interface DeviceOperationOverviewTimelineSegmentVO {
|
||||
status: 'running' | 'standby' | 'fault' | 'offline'
|
||||
startHour: number
|
||||
endHour: number
|
||||
}
|
||||
|
||||
export interface DeviceOperationOverviewTimelineRowVO {
|
||||
id: string
|
||||
name: string
|
||||
utilizationRate: number
|
||||
segments: DeviceOperationOverviewTimelineSegmentVO[]
|
||||
}
|
||||
|
||||
export interface DeviceOperationOverviewRespVO {
|
||||
metrics: DeviceOperationOverviewMetricVO[]
|
||||
hourlyStatus: DeviceOperationOverviewHourlyStatusVO[]
|
||||
summary: DeviceOperationOverviewSummaryVO[]
|
||||
summaryTotalHours: number
|
||||
timelineRows: DeviceOperationOverviewTimelineRowVO[]
|
||||
totalDevices: number
|
||||
}
|
||||
|
||||
export const DeviceOperationOverviewApi = {
|
||||
getRunOverview: async (params: DeviceOperationOverviewParams) => {
|
||||
return await request.get<DeviceOperationOverviewRespVO>({
|
||||
url: `/iot/device-operation-record/runOverview`,
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export interface CapacityReportVO {
|
||||
id: number
|
||||
deviceCode: string
|
||||
deviceName: string
|
||||
typeName: string
|
||||
deviceStatus: number
|
||||
ratedCapacity: number
|
||||
reportCapacity: number
|
||||
actualCapacity: number
|
||||
workshopName: string
|
||||
}
|
||||
|
||||
export interface CapacityReportQuery {
|
||||
pageNo: number
|
||||
pageSize: number
|
||||
deviceCode?: string
|
||||
deviceName?: string
|
||||
deviceType?: string
|
||||
deviceStatus?: string
|
||||
workshop?: string
|
||||
ids?: string
|
||||
}
|
||||
|
||||
export const DeviceLedgerApi = {
|
||||
/**
|
||||
* 产能报表分页查询
|
||||
*/
|
||||
getCapacityReportPage: async (params: CapacityReportQuery) => {
|
||||
return await request.get({ url: '/mes/device-ledger/capacity-report/page', params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 产能报表导出
|
||||
*/
|
||||
exportCapacityReport: async (params: { ids?: string }) => {
|
||||
return await request.download({ url: '/mes/device-ledger/capacity-report/export-excel', params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新设备状态
|
||||
*/
|
||||
updateDeviceLedger: async (data: { id: number; deviceStatus: number }) => {
|
||||
return await request.put({ url: '/mes/device-ledger/update', data })
|
||||
}
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export interface PrintTemplateVO {
|
||||
id: number
|
||||
templateCode: string
|
||||
templateName: string
|
||||
templateType: number
|
||||
templateBizType: string
|
||||
templateJson: string
|
||||
remark: string
|
||||
isEnable: boolean
|
||||
createTime: string
|
||||
}
|
||||
|
||||
export const PrintTemplateApi = {
|
||||
getPrintTemplatePage: async (params: any) => {
|
||||
return await request.get({ url: `/mes/print-template/page`, params })
|
||||
},
|
||||
getPrintTemplate: async (id: number) => {
|
||||
return await request.get({ url: `/mes/print-template/get?id=` + id })
|
||||
},
|
||||
createPrintTemplate: async (data: PrintTemplateVO) => {
|
||||
return await request.post({ url: `/mes/print-template/create`, data })
|
||||
},
|
||||
updatePrintTemplate: async (data: PrintTemplateVO) => {
|
||||
return await request.put({ url: `/mes/print-template/update`, data })
|
||||
},
|
||||
deletePrintTemplate: async (id: number) => {
|
||||
return await request.delete({ url: `/mes/print-template/delete?id=` + id })
|
||||
},
|
||||
exportPrintTemplate: async (params) => {
|
||||
return await request.download({ url: `/mes/print-template/export-excel`, params })
|
||||
},
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 366 B |
@ -1,5 +1,4 @@
|
||||
import Icon from './src/Icon.vue'
|
||||
import IconSelect from './src/IconSelect.vue'
|
||||
import AppIconSelect from './src/AppIconSelect.vue'
|
||||
|
||||
export { Icon, IconSelect, AppIconSelect }
|
||||
export { Icon, IconSelect }
|
||||
|
||||
@ -1,280 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { uniIconsList, uviewIconsList, uniIconsUnicodeMap, uviewIconsUnicodeMap } from './appIconData'
|
||||
|
||||
defineOptions({ name: 'AppIconSelect' })
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
require: false,
|
||||
type: String
|
||||
},
|
||||
clearable: {
|
||||
require: false,
|
||||
type: Boolean
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<{ (e: 'update:modelValue', v: string) }>()
|
||||
|
||||
const visible = ref(false)
|
||||
const inputValue = toRef(props, 'modelValue')
|
||||
const currentActiveType = ref('uni-icons')
|
||||
const filterValue = ref('')
|
||||
|
||||
const iconDataMap: Record<string, string[]> = {
|
||||
'uni-icons': uniIconsList,
|
||||
'uview-plus': uviewIconsList
|
||||
}
|
||||
|
||||
const unicodeMapMap: Record<string, Record<string, string>> = {
|
||||
'uni-icons': uniIconsUnicodeMap,
|
||||
'uview-plus': uviewIconsUnicodeMap
|
||||
}
|
||||
|
||||
const fontFamilyMap: Record<string, string> = {
|
||||
'uni-icons': 'UniIconsFontFamily',
|
||||
'uview-plus': 'uview-iconfont'
|
||||
}
|
||||
|
||||
const tabsList = [
|
||||
{ label: 'uni-icons', name: 'uni-icons' },
|
||||
{ label: 'uview-plus', name: 'uview-plus' }
|
||||
]
|
||||
|
||||
const pageSize = ref(96)
|
||||
const currentPage = ref(1)
|
||||
|
||||
const currentIconList = computed(() => {
|
||||
return iconDataMap[currentActiveType.value] || []
|
||||
})
|
||||
|
||||
const currentUnicodeMap = computed(() => {
|
||||
return unicodeMapMap[currentActiveType.value] || {}
|
||||
})
|
||||
|
||||
const currentFontFamily = computed(() => {
|
||||
return fontFamilyMap[currentActiveType.value] || 'UniIconsFontFamily'
|
||||
})
|
||||
|
||||
const filteredList = computed(() => {
|
||||
return currentIconList.value.filter((v) =>
|
||||
v.toLowerCase().includes(filterValue.value.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
const pageList = computed(() => {
|
||||
if (currentPage.value === 1) {
|
||||
return filteredList.value.slice(0, pageSize.value)
|
||||
}
|
||||
return filteredList.value.slice(
|
||||
pageSize.value * (currentPage.value - 1),
|
||||
pageSize.value * (currentPage.value - 1) + pageSize.value
|
||||
)
|
||||
})
|
||||
|
||||
const iconCount = computed(() => filteredList.value.length)
|
||||
|
||||
function parseIconValue(value: string): { prefix: string; name: string } | null {
|
||||
if (!value || value.indexOf(':') < 0) return null
|
||||
const idx = value.indexOf(':')
|
||||
return {
|
||||
prefix: value.substring(0, idx),
|
||||
name: value.substring(idx + 1)
|
||||
}
|
||||
}
|
||||
|
||||
function getUnicode(name: string): string {
|
||||
return currentUnicodeMap.value[name] || ''
|
||||
}
|
||||
|
||||
function getSelectedFontFamily(fullValue: string): string {
|
||||
const parsed = parseIconValue(fullValue)
|
||||
if (!parsed) return 'UniIconsFontFamily'
|
||||
return fontFamilyMap[parsed.prefix] || 'UniIconsFontFamily'
|
||||
}
|
||||
|
||||
function getSelectedUnicode(fullValue: string): string {
|
||||
const parsed = parseIconValue(fullValue)
|
||||
if (!parsed) return ''
|
||||
const map = unicodeMapMap[parsed.prefix]
|
||||
return map ? map[parsed.name] || '' : ''
|
||||
}
|
||||
|
||||
function handleClick({ props }: any) {
|
||||
currentPage.value = 1
|
||||
currentActiveType.value = props.name
|
||||
}
|
||||
|
||||
function onChangeIcon(item: string) {
|
||||
emit('update:modelValue', currentActiveType.value + ':' + item)
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
function onCurrentChange(page: number) {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
function clearIcon() {
|
||||
emit('update:modelValue', '')
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val) {
|
||||
const parsed = parseIconValue(val)
|
||||
if (parsed && fontFamilyMap[parsed.prefix]) {
|
||||
currentActiveType.value = parsed.prefix
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => filterValue.value,
|
||||
() => {
|
||||
currentPage.value = 1
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="selector">
|
||||
<ElInput v-model="inputValue" @click="visible = !visible" :clearable="props.clearable" @clear="clearIcon">
|
||||
<template #append>
|
||||
<ElPopover
|
||||
:visible="visible"
|
||||
:width="355"
|
||||
popper-class="pure-popper"
|
||||
trigger="click"
|
||||
>
|
||||
<template #reference>
|
||||
<div
|
||||
class="h-32px w-40px flex cursor-pointer items-center justify-center"
|
||||
@click="visible = !visible"
|
||||
>
|
||||
<span
|
||||
v-if="inputValue && getSelectedUnicode(inputValue)"
|
||||
class="app-icon-font"
|
||||
:style="{ fontFamily: getSelectedFontFamily(inputValue), fontSize: '18px' }"
|
||||
>{{ getSelectedUnicode(inputValue) }}</span>
|
||||
<span v-else class="text-12px color-gray-500">选择</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElInput v-model="filterValue" class="p-2" clearable placeholder="搜索图标" />
|
||||
<ElDivider border-style="dashed" />
|
||||
|
||||
<ElTabs v-model="currentActiveType" @tab-click="handleClick">
|
||||
<ElTabPane
|
||||
v-for="(pane, index) in tabsList"
|
||||
:key="index"
|
||||
:label="pane.label"
|
||||
:name="pane.name"
|
||||
>
|
||||
<ElDivider border-style="dashed" class="tab-divider" />
|
||||
<ElScrollbar height="220px">
|
||||
<ul class="ml-2 flex flex-wrap">
|
||||
<li
|
||||
v-for="(item, key) in pageList"
|
||||
:key="key"
|
||||
:style="inputValue === currentActiveType + ':' + item ? { borderColor: 'var(--el-color-primary)', color: 'var(--el-color-primary)' } : {}"
|
||||
:title="item"
|
||||
class="icon-item mr-2 mt-1 w-1/10 flex cursor-pointer items-center justify-center border border-solid p-2"
|
||||
@click="onChangeIcon(item)"
|
||||
>
|
||||
<span
|
||||
class="app-icon-font"
|
||||
:style="{ fontFamily: currentFontFamily }"
|
||||
>{{ getUnicode(item) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</ElScrollbar>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
<ElDivider border-style="dashed" />
|
||||
|
||||
<ElPagination
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:total="iconCount"
|
||||
background
|
||||
class="h-10 flex items-center justify-center"
|
||||
layout="prev, pager, next"
|
||||
size="small"
|
||||
@current-change="onCurrentChange"
|
||||
/>
|
||||
</ElPopover>
|
||||
</template>
|
||||
</ElInput>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@font-face {
|
||||
font-family: 'UniIconsFontFamily';
|
||||
src: url('@/assets/fonts/uniicons.ttf') format('truetype');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'uview-iconfont';
|
||||
src: url('@/assets/fonts/uview-icons.ttf') format('truetype');
|
||||
font-display: swap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-icon-font {
|
||||
font-style: normal;
|
||||
font-size: 20px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.el-divider--horizontal {
|
||||
margin: 1px auto !important;
|
||||
}
|
||||
|
||||
.tab-divider.el-divider--horizontal {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.icon-item {
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
transform: scaleX(1.05);
|
||||
transition: all 0.4s;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-next) {
|
||||
font-size: 15px;
|
||||
line-height: 32px;
|
||||
box-shadow: -5px 0 5px -6px #ccc;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-prev) {
|
||||
font-size: 15px;
|
||||
line-height: 32px;
|
||||
box-shadow: 5px 0 5px -6px #ccc;
|
||||
}
|
||||
|
||||
:deep(.el-input-group__append) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
height: 30px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__header),
|
||||
:deep(.el-tabs__nav-wrap) {
|
||||
position: static;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@ -1,382 +0,0 @@
|
||||
const uniIconsUnicodeMap: Record<string, string> = {
|
||||
'arrow-down': '\ue6be',
|
||||
'arrow-left': '\ue6bc',
|
||||
'arrow-right': '\ue6bb',
|
||||
'arrow-up': '\ue6bd',
|
||||
'auth': '\ue6ab',
|
||||
'auth-filled': '\ue6cc',
|
||||
'back': '\ue6b9',
|
||||
'bars': '\ue627',
|
||||
'calendar': '\ue6a0',
|
||||
'calendar-filled': '\ue6c0',
|
||||
'camera': '\ue65a',
|
||||
'camera-filled': '\ue658',
|
||||
'cart': '\ue631',
|
||||
'cart-filled': '\ue6d0',
|
||||
'chat': '\ue65d',
|
||||
'chat-filled': '\ue659',
|
||||
'chatboxes': '\ue696',
|
||||
'chatboxes-filled': '\ue692',
|
||||
'chatbubble': '\ue697',
|
||||
'chatbubble-filled': '\ue694',
|
||||
'checkbox': '\ue62b',
|
||||
'checkbox-filled': '\ue62c',
|
||||
'checkmarkempty': '\ue65c',
|
||||
'circle': '\ue65b',
|
||||
'circle-filled': '\ue65e',
|
||||
'clear': '\ue66d',
|
||||
'close': '\ue673',
|
||||
'closeempty': '\ue66c',
|
||||
'cloud-download': '\ue647',
|
||||
'cloud-download-filled': '\ue646',
|
||||
'cloud-upload': '\ue645',
|
||||
'cloud-upload-filled': '\ue648',
|
||||
'color': '\ue6cf',
|
||||
'color-filled': '\ue6c9',
|
||||
'compose': '\ue67f',
|
||||
'contact': '\ue693',
|
||||
'contact-filled': '\ue695',
|
||||
'down': '\ue6b8',
|
||||
'bottom': '\ue6b8',
|
||||
'download': '\ue68d',
|
||||
'download-filled': '\ue681',
|
||||
'email': '\ue69e',
|
||||
'email-filled': '\ue69a',
|
||||
'eye': '\ue651',
|
||||
'eye-filled': '\ue66a',
|
||||
'eye-slash': '\ue6b3',
|
||||
'eye-slash-filled': '\ue6b4',
|
||||
'fire': '\ue6a1',
|
||||
'fire-filled': '\ue6c5',
|
||||
'flag': '\ue65f',
|
||||
'flag-filled': '\ue660',
|
||||
'folder-add': '\ue6a9',
|
||||
'folder-add-filled': '\ue6c8',
|
||||
'font': '\ue6a3',
|
||||
'forward': '\ue6ba',
|
||||
'gear': '\ue664',
|
||||
'gear-filled': '\ue661',
|
||||
'gift': '\ue6a4',
|
||||
'gift-filled': '\ue6c4',
|
||||
'hand-down': '\ue63d',
|
||||
'hand-down-filled': '\ue63c',
|
||||
'hand-up': '\ue63f',
|
||||
'hand-up-filled': '\ue63e',
|
||||
'headphones': '\ue630',
|
||||
'heart': '\ue639',
|
||||
'heart-filled': '\ue641',
|
||||
'help': '\ue679',
|
||||
'help-filled': '\ue674',
|
||||
'home': '\ue662',
|
||||
'home-filled': '\ue663',
|
||||
'image': '\ue670',
|
||||
'image-filled': '\ue678',
|
||||
'images': '\ue650',
|
||||
'images-filled': '\ue64b',
|
||||
'info': '\ue669',
|
||||
'info-filled': '\ue649',
|
||||
'left': '\ue6b7',
|
||||
'link': '\ue6a5',
|
||||
'list': '\ue644',
|
||||
'location': '\ue6ae',
|
||||
'location-filled': '\ue6af',
|
||||
'locked': '\ue66b',
|
||||
'locked-filled': '\ue668',
|
||||
'loop': '\ue633',
|
||||
'mail-open': '\ue643',
|
||||
'mail-open-filled': '\ue63a',
|
||||
'map': '\ue667',
|
||||
'map-filled': '\ue666',
|
||||
'map-pin': '\ue6ad',
|
||||
'map-pin-ellipse': '\ue6ac',
|
||||
'medal': '\ue6a2',
|
||||
'medal-filled': '\ue6c3',
|
||||
'mic': '\ue671',
|
||||
'mic-filled': '\ue677',
|
||||
'micoff': '\ue67e',
|
||||
'micoff-filled': '\ue6b0',
|
||||
'minus': '\ue66f',
|
||||
'minus-filled': '\ue67d',
|
||||
'more': '\ue64d',
|
||||
'more-filled': '\ue64e',
|
||||
'navigate': '\ue66e',
|
||||
'navigate-filled': '\ue67a',
|
||||
'notification': '\ue6a6',
|
||||
'notification-filled': '\ue6c1',
|
||||
'paperclip': '\ue652',
|
||||
'paperplane': '\ue672',
|
||||
'paperplane-filled': '\ue675',
|
||||
'person': '\ue699',
|
||||
'person-filled': '\ue69d',
|
||||
'personadd': '\ue69f',
|
||||
'personadd-filled': '\ue698',
|
||||
'phone': '\ue69c',
|
||||
'phone-filled': '\ue69b',
|
||||
'plus': '\ue676',
|
||||
'plus-filled': '\ue6c7',
|
||||
'plusempty': '\ue67b',
|
||||
'pulldown': '\ue632',
|
||||
'pyq': '\ue682',
|
||||
'qq': '\ue680',
|
||||
'redo': '\ue64a',
|
||||
'redo-filled': '\ue655',
|
||||
'refresh': '\ue657',
|
||||
'refresh-filled': '\ue656',
|
||||
'refreshempty': '\ue6bf',
|
||||
'reload': '\ue6b2',
|
||||
'right': '\ue6b5',
|
||||
'scan': '\ue62a',
|
||||
'search': '\ue654',
|
||||
'settings': '\ue653',
|
||||
'settings-filled': '\ue6ce',
|
||||
'shop': '\ue62f',
|
||||
'shop-filled': '\ue6cd',
|
||||
'smallcircle': '\ue67c',
|
||||
'smallcircle-filled': '\ue665',
|
||||
'sound': '\ue684',
|
||||
'sound-filled': '\ue686',
|
||||
'spinner-cycle': '\ue68a',
|
||||
'staff': '\ue6a7',
|
||||
'staff-filled': '\ue6cb',
|
||||
'star': '\ue688',
|
||||
'star-filled': '\ue68f',
|
||||
'starhalf': '\ue683',
|
||||
'trash': '\ue687',
|
||||
'trash-filled': '\ue685',
|
||||
'tune': '\ue6aa',
|
||||
'tune-filled': '\ue6ca',
|
||||
'undo': '\ue64f',
|
||||
'undo-filled': '\ue64c',
|
||||
'up': '\ue6b6',
|
||||
'top': '\ue6b6',
|
||||
'upload': '\ue690',
|
||||
'upload-filled': '\ue68e',
|
||||
'videocam': '\ue68c',
|
||||
'videocam-filled': '\ue689',
|
||||
'vip': '\ue6a8',
|
||||
'vip-filled': '\ue6c6',
|
||||
'wallet': '\ue6b1',
|
||||
'wallet-filled': '\ue6c2',
|
||||
'weibo': '\ue68b',
|
||||
'weixin': '\ue691'
|
||||
}
|
||||
|
||||
const uviewIconsUnicodeMap: Record<string, string> = {
|
||||
'level': '\ue693',
|
||||
'column-line': '\ue68e',
|
||||
'checkbox-mark': '\ue807',
|
||||
'folder': '\ue7f5',
|
||||
'movie': '\ue7f6',
|
||||
'star-fill': '\ue669',
|
||||
'star': '\ue65f',
|
||||
'phone-fill': '\ue64f',
|
||||
'phone': '\ue622',
|
||||
'apple-fill': '\ue881',
|
||||
'chrome-circle-fill': '\ue885',
|
||||
'backspace': '\ue67b',
|
||||
'attach': '\ue632',
|
||||
'cut': '\ue948',
|
||||
'empty-car': '\ue602',
|
||||
'empty-coupon': '\ue682',
|
||||
'empty-address': '\ue646',
|
||||
'empty-favor': '\ue67c',
|
||||
'empty-permission': '\ue686',
|
||||
'empty-news': '\ue687',
|
||||
'empty-search': '\ue664',
|
||||
'github-circle-fill': '\ue887',
|
||||
'rmb': '\ue608',
|
||||
'person-delete-fill': '\ue66a',
|
||||
'reload': '\ue788',
|
||||
'order': '\ue68f',
|
||||
'server-man': '\ue6bc',
|
||||
'search': '\ue62a',
|
||||
'fingerprint': '\ue955',
|
||||
'more-dot-fill': '\ue630',
|
||||
'scan': '\ue662',
|
||||
'share-square': '\ue60b',
|
||||
'map': '\ue61d',
|
||||
'map-fill': '\ue64e',
|
||||
'tags': '\ue629',
|
||||
'tags-fill': '\ue651',
|
||||
'bookmark-fill': '\ue63b',
|
||||
'bookmark': '\ue60a',
|
||||
'eye': '\ue613',
|
||||
'eye-fill': '\ue641',
|
||||
'mic': '\ue64a',
|
||||
'mic-off': '\ue649',
|
||||
'calendar': '\ue66e',
|
||||
'calendar-fill': '\ue634',
|
||||
'trash': '\ue623',
|
||||
'trash-fill': '\ue658',
|
||||
'play-left': '\ue66d',
|
||||
'play-right': '\ue610',
|
||||
'minus': '\ue618',
|
||||
'plus': '\ue62d',
|
||||
'info': '\ue653',
|
||||
'info-circle': '\ue7d2',
|
||||
'info-circle-fill': '\ue64b',
|
||||
'question': '\ue715',
|
||||
'error': '\ue6d3',
|
||||
'close': '\ue685',
|
||||
'checkmark': '\ue6a8',
|
||||
'android-circle-fill': '\ue67e',
|
||||
'android-fill': '\ue67d',
|
||||
'ie': '\ue87b',
|
||||
'IE-circle-fill': '\ue889',
|
||||
'google': '\ue87a',
|
||||
'google-circle-fill': '\ue88a',
|
||||
'setting-fill': '\ue872',
|
||||
'setting': '\ue61f',
|
||||
'minus-square-fill': '\ue855',
|
||||
'plus-square-fill': '\ue856',
|
||||
'heart': '\ue7df',
|
||||
'heart-fill': '\ue851',
|
||||
'camera': '\ue7d7',
|
||||
'camera-fill': '\ue870',
|
||||
'more-circle': '\ue63e',
|
||||
'more-circle-fill': '\ue645',
|
||||
'chat': '\ue620',
|
||||
'chat-fill': '\ue61e',
|
||||
'bag-fill': '\ue617',
|
||||
'bag': '\ue619',
|
||||
'error-circle-fill': '\ue62c',
|
||||
'error-circle': '\ue624',
|
||||
'close-circle': '\ue63f',
|
||||
'close-circle-fill': '\ue637',
|
||||
'checkmark-circle': '\ue63d',
|
||||
'checkmark-circle-fill': '\ue635',
|
||||
'question-circle-fill': '\ue666',
|
||||
'question-circle': '\ue625',
|
||||
'share': '\ue631',
|
||||
'share-fill': '\ue65e',
|
||||
'shopping-cart': '\ue621',
|
||||
'shopping-cart-fill': '\ue65d',
|
||||
'bell': '\ue609',
|
||||
'bell-fill': '\ue640',
|
||||
'list': '\ue650',
|
||||
'list-dot': '\ue616',
|
||||
'zhihu': '\ue6ba',
|
||||
'zhihu-circle-fill': '\ue709',
|
||||
'zhifubao': '\ue6b9',
|
||||
'zhifubao-circle-fill': '\ue6b8',
|
||||
'weixin-circle-fill': '\ue6b1',
|
||||
'weixin-fill': '\ue6b2',
|
||||
'twitter-circle-fill': '\ue6ab',
|
||||
'twitter': '\ue6aa',
|
||||
'taobao-circle-fill': '\ue6a7',
|
||||
'taobao': '\ue6a6',
|
||||
'weibo-circle-fill': '\ue6a5',
|
||||
'weibo': '\ue6a4',
|
||||
'qq-fill': '\ue6a1',
|
||||
'qq-circle-fill': '\ue6a0',
|
||||
'moments-circel-fill': '\ue69a',
|
||||
'moments': '\ue69b',
|
||||
'qzone': '\ue695',
|
||||
'qzone-circle-fill': '\ue696',
|
||||
'baidu-circle-fill': '\ue680',
|
||||
'baidu': '\ue681',
|
||||
'facebook-circle-fill': '\ue68a',
|
||||
'facebook': '\ue689',
|
||||
'car': '\ue60c',
|
||||
'car-fill': '\ue636',
|
||||
'warning-fill': '\ue64d',
|
||||
'warning': '\ue694',
|
||||
'clock-fill': '\ue638',
|
||||
'clock': '\ue60f',
|
||||
'edit-pen': '\ue612',
|
||||
'edit-pen-fill': '\ue66b',
|
||||
'email': '\ue611',
|
||||
'email-fill': '\ue642',
|
||||
'minus-circle': '\ue61b',
|
||||
'minus-circle-fill': '\ue652',
|
||||
'plus-circle': '\ue62e',
|
||||
'plus-circle-fill': '\ue661',
|
||||
'file-text': '\ue663',
|
||||
'file-text-fill': '\ue665',
|
||||
'pushpin': '\ue7e3',
|
||||
'pushpin-fill': '\ue86e',
|
||||
'grid': '\ue673',
|
||||
'grid-fill': '\ue678',
|
||||
'play-circle': '\ue647',
|
||||
'play-circle-fill': '\ue655',
|
||||
'pause-circle-fill': '\ue654',
|
||||
'pause': '\ue8fa',
|
||||
'pause-circle': '\ue643',
|
||||
'eye-off': '\ue648',
|
||||
'eye-off-outline': '\ue62b',
|
||||
'gift-fill': '\ue65c',
|
||||
'gift': '\ue65b',
|
||||
'rmb-circle-fill': '\ue657',
|
||||
'rmb-circle': '\ue677',
|
||||
'kefu-ermai': '\ue656',
|
||||
'server-fill': '\ue751',
|
||||
'coupon-fill': '\ue8c4',
|
||||
'coupon': '\ue8ae',
|
||||
'integral': '\ue704',
|
||||
'integral-fill': '\ue703',
|
||||
'home-fill': '\ue964',
|
||||
'home': '\ue965',
|
||||
'hourglass-half-fill': '\ue966',
|
||||
'hourglass': '\ue967',
|
||||
'account': '\ue628',
|
||||
'plus-people-fill': '\ue626',
|
||||
'minus-people-fill': '\ue615',
|
||||
'account-fill': '\ue614',
|
||||
'thumb-down-fill': '\ue726',
|
||||
'thumb-down': '\ue727',
|
||||
'thumb-up': '\ue733',
|
||||
'thumb-up-fill': '\ue72f',
|
||||
'lock-fill': '\ue979',
|
||||
'lock-open': '\ue973',
|
||||
'lock-opened-fill': '\ue974',
|
||||
'lock': '\ue97a',
|
||||
'red-packet-fill': '\ue690',
|
||||
'photo-fill': '\ue98b',
|
||||
'photo': '\ue98d',
|
||||
'volume-off-fill': '\ue659',
|
||||
'volume-off': '\ue644',
|
||||
'volume-fill': '\ue670',
|
||||
'volume': '\ue633',
|
||||
'red-packet': '\ue691',
|
||||
'download': '\ue63c',
|
||||
'arrow-up-fill': '\ue6b0',
|
||||
'arrow-down-fill': '\ue600',
|
||||
'play-left-fill': '\ue675',
|
||||
'play-right-fill': '\ue676',
|
||||
'rewind-left-fill': '\ue679',
|
||||
'rewind-right-fill': '\ue67a',
|
||||
'arrow-downward': '\ue604',
|
||||
'arrow-leftward': '\ue601',
|
||||
'arrow-rightward': '\ue603',
|
||||
'arrow-upward': '\ue607',
|
||||
'arrow-down': '\ue60d',
|
||||
'arrow-right': '\ue605',
|
||||
'arrow-left': '\ue60e',
|
||||
'arrow-up': '\ue606',
|
||||
'skip-back-left': '\ue674',
|
||||
'skip-forward-right': '\ue672',
|
||||
'rewind-right': '\ue66f',
|
||||
'rewind-left': '\ue671',
|
||||
'arrow-right-double': '\ue68d',
|
||||
'arrow-left-double': '\ue68c',
|
||||
'wifi-off': '\ue668',
|
||||
'wifi': '\ue667',
|
||||
'empty-data': '\ue62f',
|
||||
'empty-history': '\ue684',
|
||||
'empty-list': '\ue68b',
|
||||
'empty-page': '\ue627',
|
||||
'empty-order': '\ue639',
|
||||
'man': '\ue697',
|
||||
'woman': '\ue69c',
|
||||
'man-add': '\ue61c',
|
||||
'man-add-fill': '\ue64c',
|
||||
'man-delete': '\ue61a',
|
||||
'man-delete-fill': '\ue66a',
|
||||
'zh': '\ue70a',
|
||||
'en': '\ue692'
|
||||
}
|
||||
|
||||
const uniIconsList = Object.keys(uniIconsUnicodeMap)
|
||||
const uviewIconsList = Object.keys(uviewIconsUnicodeMap)
|
||||
|
||||
export { uniIconsList, uviewIconsList, uniIconsUnicodeMap, uviewIconsUnicodeMap }
|
||||
@ -1,371 +0,0 @@
|
||||
<template>
|
||||
<div class="qrcode-action-card">
|
||||
<el-image
|
||||
v-if="imageUrl"
|
||||
:src="imageUrl"
|
||||
:preview-src-list="[imageUrl]"
|
||||
preview-teleported
|
||||
fit="cover"
|
||||
class="qrcode-action-card__img"
|
||||
>
|
||||
<template #error>
|
||||
<div class="qrcode-action-card__error">{{ errorText || emptyText }}</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else class="qrcode-action-card__error">{{ emptyText }}</div>
|
||||
<div v-if="showActionMask" class="qrcode-action-card__mask">
|
||||
<el-button v-if="showPreview" circle :disabled="!imageUrl" @click="handlePreview">
|
||||
<Icon icon="ep:zoom-in" />
|
||||
</el-button>
|
||||
<el-button v-if="showPrint" circle :disabled="!imageUrl || printDisabled" @click="handlePrint">
|
||||
<Icon icon="ep:printer" />
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="showRefresh"
|
||||
circle
|
||||
:loading="refreshLoading"
|
||||
:disabled="refreshDisabled || refreshLoading"
|
||||
@click="handleRefresh"
|
||||
>
|
||||
<Icon icon="ep:refresh" />
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<HiprintPreviewDialog ref="hiprintPreviewDialogRef" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import request from '@/config/axios'
|
||||
import HiprintPreviewDialog from '@/components/HiprintPreviewDialog/index.vue'
|
||||
import { createImageViewer } from '@/components/ImageViewer'
|
||||
import { ElLoading } from 'element-plus'
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
imageUrl?: string
|
||||
printId?: string | number
|
||||
printTitle?: string
|
||||
emptyText?: string
|
||||
refreshUrl?: string
|
||||
refreshMethod?: 'post' | 'put' | 'get'
|
||||
refreshDisabled?: boolean
|
||||
printDisabled?: boolean
|
||||
refreshConfirmText?: string
|
||||
errorText?: string
|
||||
showPreview?: boolean
|
||||
showPrint?: boolean
|
||||
showRefresh?: boolean
|
||||
printAdaptive?: boolean
|
||||
printPaperWidth?: number
|
||||
printPaperHeight?: number
|
||||
printMaxWidth?: number
|
||||
printMaxHeight?: number
|
||||
templateJsonUrl?: string
|
||||
templateJson?: any
|
||||
printData?: Record<string, any>
|
||||
}>(),
|
||||
{
|
||||
imageUrl: '',
|
||||
printId: '',
|
||||
printTitle: '二维码打印预览',
|
||||
emptyText: '',
|
||||
refreshUrl: '',
|
||||
refreshMethod: 'post',
|
||||
refreshDisabled: false,
|
||||
printDisabled: false,
|
||||
refreshConfirmText: '',
|
||||
errorText: '',
|
||||
showPreview: true,
|
||||
showPrint: true,
|
||||
showRefresh: true,
|
||||
printAdaptive: true,
|
||||
printPaperWidth: 80,
|
||||
printPaperHeight: 80,
|
||||
printMaxWidth: 180,
|
||||
printMaxHeight: 120,
|
||||
templateJsonUrl: '',
|
||||
templateJson: undefined,
|
||||
printData: () => ({})
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'refresh-success', data: any): void
|
||||
}>()
|
||||
|
||||
const hiprintPreviewDialogRef = ref()
|
||||
const refreshLoading = ref(false)
|
||||
|
||||
const showActionMask = computed(() => props.showPreview || props.showPrint || props.showRefresh)
|
||||
|
||||
const buildQrcodeTemplateJson = (
|
||||
qrcodeUrl: string,
|
||||
printId: string,
|
||||
imageWidth: number,
|
||||
imageHeight: number,
|
||||
paperHeight: number
|
||||
) => ({
|
||||
panels: [
|
||||
{
|
||||
index: 0,
|
||||
name: 1,
|
||||
width: imageWidth,
|
||||
height: paperHeight,
|
||||
paperHeader: 0,
|
||||
paperFooter: 0,
|
||||
printElements: [
|
||||
{
|
||||
options: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
src: qrcodeUrl,
|
||||
field: 'qrcodeUrl',
|
||||
title: '二维码图片',
|
||||
testData: qrcodeUrl
|
||||
},
|
||||
printElementType: {
|
||||
title: '图片',
|
||||
type: 'image'
|
||||
}
|
||||
},
|
||||
{
|
||||
options: {
|
||||
left: 0,
|
||||
top: imageHeight + 2,
|
||||
height: 10,
|
||||
width: imageWidth,
|
||||
testData: printId,
|
||||
title: 'ID',
|
||||
field: 'printId',
|
||||
textAlign: 'center',
|
||||
fontSize: 10
|
||||
},
|
||||
printElementType: {
|
||||
title: '文本',
|
||||
type: 'text'
|
||||
}
|
||||
}
|
||||
],
|
||||
paperNumberDisabled: true,
|
||||
paperNumberContinue: true,
|
||||
watermarkOptions: {},
|
||||
panelLayoutOptions: {}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const getImageSize = (src: string) =>
|
||||
new Promise<{ width: number; height: number }>((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
const width = img.naturalWidth || img.width
|
||||
const height = img.naturalHeight || img.height
|
||||
if (!width || !height) {
|
||||
reject(new Error('invalid image size'))
|
||||
return
|
||||
}
|
||||
resolve({ width, height })
|
||||
}
|
||||
img.onerror = () => reject(new Error('image load failed'))
|
||||
img.src = src
|
||||
})
|
||||
|
||||
const resolvePrintSize = async (src: string) => {
|
||||
const fallbackWidth = Math.max(20, Number(props.printPaperWidth) || 80)
|
||||
const fallbackHeight = Math.max(20, Number(props.printPaperHeight) || 80)
|
||||
if (!props.printAdaptive) {
|
||||
return { width: fallbackWidth, height: fallbackHeight }
|
||||
}
|
||||
try {
|
||||
const size = await getImageSize(src)
|
||||
const ratio = size.width / size.height
|
||||
if (!Number.isFinite(ratio) || ratio <= 0) {
|
||||
return { width: fallbackWidth, height: fallbackHeight }
|
||||
}
|
||||
if (ratio >= 1) {
|
||||
const height = fallbackHeight
|
||||
const width = Math.min(Math.max(fallbackWidth, Math.round(height * ratio)), Number(props.printMaxWidth) || 180)
|
||||
return { width, height }
|
||||
}
|
||||
const width = fallbackWidth
|
||||
const height = Math.min(Math.max(fallbackHeight, Math.round(width / ratio)), Number(props.printMaxHeight) || 120)
|
||||
return { width, height }
|
||||
} catch {
|
||||
return { width: fallbackWidth, height: fallbackHeight }
|
||||
}
|
||||
}
|
||||
|
||||
const handlePreview = () => {
|
||||
if (!props.imageUrl) return
|
||||
createImageViewer({
|
||||
zIndex: 9999999,
|
||||
urlList: [props.imageUrl]
|
||||
})
|
||||
}
|
||||
|
||||
const replaceTemplateValues = (templateJson: any, printData: Record<string, any>) => {
|
||||
if (!templateJson?.panels) return templateJson
|
||||
|
||||
return {
|
||||
...templateJson,
|
||||
panels: templateJson.panels.map((panel: any) => ({
|
||||
...panel,
|
||||
printElements: panel.printElements?.map((element: any) => {
|
||||
if (!element?.options?.qid) return element
|
||||
|
||||
const qid = element.options.qid
|
||||
const value = printData[qid]
|
||||
|
||||
if (value === undefined || value === null) return element
|
||||
|
||||
const newOptions = { ...element.options }
|
||||
|
||||
if (qid === 'qrcodeUrl') {
|
||||
newOptions.src = value
|
||||
newOptions.testData = value
|
||||
} else {
|
||||
newOptions.field = qid
|
||||
if (newOptions.testData === undefined || newOptions.testData === '') {
|
||||
newOptions.testData = String(value)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...element,
|
||||
options: newOptions
|
||||
}
|
||||
}) || []
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
const handlePrint = async () => {
|
||||
if (!props.imageUrl || !props.showPrint) return
|
||||
|
||||
let templateJson: any
|
||||
let printData: Record<string, any>
|
||||
|
||||
printData = {
|
||||
qrcodeUrl: props.imageUrl,
|
||||
printId: props.printId === undefined || props.printId === null ? '' : String(props.printId),
|
||||
...props.printData
|
||||
}
|
||||
|
||||
if (props.templateJson) {
|
||||
templateJson = replaceTemplateValues(props.templateJson, printData)
|
||||
} else if (props.templateJsonUrl) {
|
||||
try {
|
||||
const response = await request.get({ url: props.templateJsonUrl })
|
||||
templateJson = typeof response.data === 'string' ? JSON.parse(response.data) : response.data
|
||||
templateJson = replaceTemplateValues(templateJson, printData)
|
||||
} catch (error) {
|
||||
console.error('获取打印模板失败', error)
|
||||
message.error('获取打印模板失败')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
const printSize = await resolvePrintSize(props.imageUrl)
|
||||
const imageWidth = printSize.width
|
||||
const imageHeight = printSize.height
|
||||
const printId = props.printId === undefined || props.printId === null ? '' : String(props.printId)
|
||||
const idAreaHeight = printId ? 14 : 0
|
||||
const paperHeight = imageHeight + idAreaHeight
|
||||
|
||||
templateJson = buildQrcodeTemplateJson(props.imageUrl, printId, imageWidth, imageHeight, paperHeight)
|
||||
}
|
||||
|
||||
const paperSize = templateJson?.panels?.[0] ? {
|
||||
width: templateJson.panels[0].width,
|
||||
height: templateJson.panels[0].height
|
||||
} : { width: 80, height: 80 }
|
||||
|
||||
hiprintPreviewDialogRef.value?.open({
|
||||
title: props.printTitle,
|
||||
printData,
|
||||
templateJson,
|
||||
withDefaultQrcodeLayout: false,
|
||||
paperSize
|
||||
})
|
||||
}
|
||||
|
||||
const handleRefresh = async () => {
|
||||
if (!props.refreshUrl || props.refreshDisabled || !props.showRefresh) return
|
||||
try {
|
||||
await message.confirm(props.refreshConfirmText || '确认刷新二维码吗?')
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
refreshLoading.value = true
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: t('common.loading'),
|
||||
background: 'rgba(0, 0, 0, 0.3)'
|
||||
})
|
||||
try {
|
||||
let data
|
||||
if (props.refreshMethod === 'get') {
|
||||
data = await request.get({ url: props.refreshUrl })
|
||||
} else if (props.refreshMethod === 'put') {
|
||||
data = await request.put({ url: props.refreshUrl })
|
||||
} else {
|
||||
data = await request.post({ url: props.refreshUrl })
|
||||
}
|
||||
emit('refresh-success', data)
|
||||
message.success(t('common.updateSuccess'))
|
||||
} finally {
|
||||
loading.close()
|
||||
refreshLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.qrcode-action-card {
|
||||
position: relative;
|
||||
height: 150px;
|
||||
width: fit-content;
|
||||
min-width: 150px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
background: var(--el-fill-color-blank);
|
||||
}
|
||||
|
||||
.qrcode-action-card__img {
|
||||
height: 150px;
|
||||
width: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.qrcode-action-card__error {
|
||||
height: 150px;
|
||||
min-width: 150px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.qrcode-action-card__mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.qrcode-action-card:hover .qrcode-action-card__mask {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,148 +0,0 @@
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item :label="t('ErpStock.WarehouseArea.warehouseId')" prop="warehouseId">
|
||||
<el-select
|
||||
v-model="formData.warehouseId"
|
||||
:placeholder="t('ErpStock.WarehouseArea.placeholderWarehouseId')"
|
||||
class="!w-1/1"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in warehouseList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseArea.areaCode')" prop="areaCode">
|
||||
<el-row :gutter="10" class="!w-full">
|
||||
<el-col :xs="24" :sm="18" :md="16" :lg="14" :xl="12">
|
||||
<el-input v-model="formData.areaCode" :placeholder="t('ErpStock.WarehouseArea.placeholderAreaCode')" :disabled="formData.isCode == true || formType === 'update'" />
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="6" :md="4" :lg="3" :xl="2">
|
||||
<div>
|
||||
<el-switch v-model="formData.isCode" :disabled="formType === 'update'" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseArea.areaName')" prop="areaName">
|
||||
<el-input v-model="formData.areaName" :placeholder="t('ErpStock.WarehouseArea.placeholderAreaName')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseArea.areaSize')" prop="areaSize">
|
||||
<el-input-number
|
||||
v-model="formData.areaSize"
|
||||
:placeholder="t('ErpStock.WarehouseArea.placeholderAreaSize')"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
class="!w-1/1"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseArea.description')" prop="description">
|
||||
<el-input type="textarea" v-model="formData.description" :placeholder="t('ErpStock.WarehouseArea.placeholderDescription')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseArea.status')" prop="status">
|
||||
<el-switch v-model="formData.status" :active-value="CommonStatusEnum.ENABLE" :inactive-value="CommonStatusEnum.DISABLE" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">{{ t('common.ok') }}</el-button>
|
||||
<el-button @click="dialogVisible = false">{{ t('common.cancel') }}</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { WarehouseAreaApi, WarehouseAreaVO } from '@/api/erp/stock/warehousearea'
|
||||
import { WarehouseApi } from '@/api/erp/stock/warehouse'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
|
||||
defineOptions({ name: 'WarehouseAreaForm' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formLoading = ref(false)
|
||||
const formType = ref('')
|
||||
const formData = ref({
|
||||
id: undefined,
|
||||
warehouseId: undefined,
|
||||
areaCode: undefined,
|
||||
areaName: undefined,
|
||||
areaSize: undefined,
|
||||
description: undefined,
|
||||
status: undefined,
|
||||
isCode: true
|
||||
})
|
||||
const formRules = reactive({
|
||||
warehouseId: [{ required: true, message: t('ErpStock.WarehouseArea.validatorWarehouseIdRequired'), trigger: 'blur' }],
|
||||
areaName: [{ required: true, message: t('ErpStock.WarehouseArea.validatorAreaNameRequired'), trigger: 'blur' }],
|
||||
status: [{ required: true, message: t('ErpStock.WarehouseArea.validatorStatusRequired'), trigger: 'blur' }]
|
||||
})
|
||||
const formRef = ref()
|
||||
const warehouseList = ref<any[]>([])
|
||||
|
||||
const open = async (type: string, id?: number) => {
|
||||
dialogVisible.value = true
|
||||
dialogTitle.value = t('action.' + type)
|
||||
formType.value = type
|
||||
resetForm()
|
||||
warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
|
||||
if (id) {
|
||||
formLoading.value = true
|
||||
try {
|
||||
formData.value = await WarehouseAreaApi.getWarehouseArea(id)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
defineExpose({ open })
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
const submitForm = async () => {
|
||||
await formRef.value.validate()
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = { ...formData.value } as unknown as WarehouseAreaVO
|
||||
if (data.isCode) {
|
||||
data.areaCode = undefined
|
||||
}
|
||||
if (formType.value === 'create') {
|
||||
await WarehouseAreaApi.createWarehouseArea(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await WarehouseAreaApi.updateWarehouseArea(data)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
warehouseId: undefined,
|
||||
areaCode: undefined,
|
||||
areaName: undefined,
|
||||
areaSize: undefined,
|
||||
description: undefined,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
isCode: true
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
@ -1,213 +0,0 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="auto"
|
||||
>
|
||||
<el-form-item :label="t('ErpStock.WarehouseArea.warehouseId')" prop="warehouseId">
|
||||
<el-select
|
||||
v-model="queryParams.warehouseId"
|
||||
:placeholder="t('ErpStock.WarehouseArea.placeholderWarehouseId')"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in warehouseList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseArea.areaCode')" prop="areaCode">
|
||||
<el-input
|
||||
v-model="queryParams.areaCode"
|
||||
:placeholder="t('ErpStock.WarehouseArea.placeholderAreaCode')"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseArea.areaName')" prop="areaName">
|
||||
<el-input
|
||||
v-model="queryParams.areaName"
|
||||
:placeholder="t('ErpStock.WarehouseArea.placeholderAreaName')"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseArea.status')" prop="status" v-show="showAllFilters">
|
||||
<el-select
|
||||
v-model="queryParams.status"
|
||||
:placeholder="t('ErpStock.WarehouseArea.placeholderStatus')"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="filterCount > 3">
|
||||
<el-button type="text" class="text-primary" @click="toggleFilters">
|
||||
<Icon :icon="showAllFilters ? 'ep:arrow-up' : 'ep:arrow-down'" class="mr-5px" />
|
||||
{{ showAllFilters ? t('FactoryModeling.FactoryStructure.collapseText') :
|
||||
t('FactoryModeling.FactoryStructure.expandText') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> {{ t('common.query') }}</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> {{ t('common.reset') }}</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="openForm('create')"
|
||||
v-hasPermi="['erp:warehouse-area:create']"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> {{ t('action.add') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<ContentWrap>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
:stripe="true"
|
||||
:show-overflow-tooltip="true"
|
||||
row-key="id"
|
||||
>
|
||||
<el-table-column :label="t('ErpStock.WarehouseArea.warehouseId')" align="center" prop="warehouseId" sortable>
|
||||
<template #default="scope">
|
||||
{{ getWarehouseName(scope.row.warehouseId) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('ErpStock.WarehouseArea.areaCode')" align="center" prop="areaCode" sortable />
|
||||
<el-table-column :label="t('ErpStock.WarehouseArea.areaName')" align="center" prop="areaName" sortable />
|
||||
<el-table-column :label="t('ErpStock.WarehouseArea.areaSize')" align="center" prop="areaSize" />
|
||||
<el-table-column :label="t('ErpStock.WarehouseArea.description')" align="center" prop="description" />
|
||||
<el-table-column :label="t('ErpStock.WarehouseArea.status')" align="center" prop="status" sortable>
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('ErpStock.WarehouseArea.createTime')" align="center" prop="createTime" :formatter="dateFormatter" width="180px" sortable />
|
||||
<el-table-column :label="t('common.operate')" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
v-hasPermi="['erp:warehouse-area:update']"
|
||||
>
|
||||
{{ t('action.edit') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['erp:warehouse-area:delete']"
|
||||
>
|
||||
{{ t('action.del') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<WarehouseAreaForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { WarehouseAreaApi, WarehouseAreaVO } from '@/api/erp/stock/warehousearea'
|
||||
import { WarehouseApi } from '@/api/erp/stock/warehouse'
|
||||
import WarehouseAreaForm from './WarehouseAreaForm.vue'
|
||||
|
||||
defineOptions({ name: 'ErpWarehouseArea' })
|
||||
|
||||
const message = useMessage()
|
||||
const { t } = useI18n()
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref<WarehouseAreaVO[]>([])
|
||||
const total = ref(0)
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
warehouseId: undefined,
|
||||
areaCode: undefined,
|
||||
areaName: undefined,
|
||||
status: undefined
|
||||
})
|
||||
const queryFormRef = ref()
|
||||
const showAllFilters = ref(false)
|
||||
const filterCount = 4
|
||||
const toggleFilters = () => {
|
||||
showAllFilters.value = !showAllFilters.value
|
||||
}
|
||||
const warehouseList = ref<any[]>([])
|
||||
|
||||
const getWarehouseName = (warehouseId: number) => {
|
||||
return warehouseList.value.find((w) => w.id === warehouseId)?.name ?? '-'
|
||||
}
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await WarehouseAreaApi.getWarehouseAreaPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getWarehouseList = async () => {
|
||||
warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await message.delConfirm()
|
||||
await WarehouseAreaApi.deleteWarehouseArea(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getWarehouseList()
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
@ -1,227 +0,0 @@
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item :label="t('ErpStock.WarehouseLocation.warehouseId')" prop="warehouseId">
|
||||
<el-select
|
||||
v-model="formData.warehouseId"
|
||||
:placeholder="t('ErpStock.WarehouseLocation.placeholderWarehouseId')"
|
||||
class="!w-1/1"
|
||||
@change="handleWarehouseChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in warehouseList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseLocation.areaId')" prop="areaId">
|
||||
<el-select
|
||||
v-model="formData.areaId"
|
||||
:placeholder="t('ErpStock.WarehouseLocation.placeholderAreaId')"
|
||||
class="!w-1/1"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in filteredAreaList"
|
||||
:key="item.id"
|
||||
:label="item.areaName"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseLocation.code')" prop="code">
|
||||
<el-row :gutter="10" class="!w-full">
|
||||
<el-col :xs="24" :sm="18" :md="16" :lg="14" :xl="12">
|
||||
<el-input v-model="formData.code" :placeholder="t('ErpStock.WarehouseLocation.placeholderCode')" :disabled="formData.isCode == true || formType === 'update'" />
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="6" :md="4" :lg="3" :xl="2">
|
||||
<div>
|
||||
<el-switch v-model="formData.isCode" :disabled="formType === 'update'" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseLocation.name')" prop="name">
|
||||
<el-input v-model="formData.name" :placeholder="t('ErpStock.WarehouseLocation.placeholderName')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseLocation.areaSize')" prop="areaSize">
|
||||
<el-input-number
|
||||
v-model="formData.areaSize"
|
||||
:placeholder="t('ErpStock.WarehouseLocation.placeholderAreaSize')"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
class="!w-1/1"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseLocation.maxLoadWeight')" prop="maxLoadWeight">
|
||||
<el-input-number
|
||||
v-model="formData.maxLoadWeight"
|
||||
:placeholder="t('ErpStock.WarehouseLocation.placeholderMaxLoadWeight')"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
class="!w-1/1"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseLocation.positionX')" prop="positionX">
|
||||
<el-input-number
|
||||
v-model="formData.positionX"
|
||||
:placeholder="t('ErpStock.WarehouseLocation.placeholderPositionX')"
|
||||
:precision="0"
|
||||
class="!w-1/1"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseLocation.positionY')" prop="positionY">
|
||||
<el-input-number
|
||||
v-model="formData.positionY"
|
||||
:placeholder="t('ErpStock.WarehouseLocation.placeholderPositionY')"
|
||||
:precision="0"
|
||||
class="!w-1/1"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseLocation.positionZ')" prop="positionZ">
|
||||
<el-input-number
|
||||
v-model="formData.positionZ"
|
||||
:placeholder="t('ErpStock.WarehouseLocation.placeholderPositionZ')"
|
||||
:precision="0"
|
||||
class="!w-1/1"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseLocation.allowProductMix')" prop="allowProductMix">
|
||||
<el-switch v-model="formData.allowProductMix" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseLocation.allowBatchMix')" prop="allowBatchMix">
|
||||
<el-switch v-model="formData.allowBatchMix" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseLocation.status')" prop="status">
|
||||
<el-switch v-model="formData.status" :active-value="CommonStatusEnum.ENABLE" :inactive-value="CommonStatusEnum.DISABLE" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">{{ t('common.ok') }}</el-button>
|
||||
<el-button @click="dialogVisible = false">{{ t('common.cancel') }}</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { WarehouseLocationApi, WarehouseLocationVO } from '@/api/erp/stock/warehouselocation'
|
||||
import { WarehouseApi } from '@/api/erp/stock/warehouse'
|
||||
import { WarehouseAreaApi } from '@/api/erp/stock/warehousearea'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
|
||||
defineOptions({ name: 'WarehouseLocationForm' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formLoading = ref(false)
|
||||
const formType = ref('')
|
||||
const formData = ref({
|
||||
id: undefined,
|
||||
warehouseId: undefined,
|
||||
areaId: undefined,
|
||||
code: undefined,
|
||||
name: undefined,
|
||||
areaSize: undefined,
|
||||
maxLoadWeight: undefined,
|
||||
positionX: undefined,
|
||||
positionY: undefined,
|
||||
positionZ: undefined,
|
||||
allowProductMix: false,
|
||||
allowBatchMix: false,
|
||||
status: undefined,
|
||||
isCode: true
|
||||
})
|
||||
const formRules = reactive({
|
||||
warehouseId: [{ required: true, message: t('ErpStock.WarehouseLocation.validatorWarehouseIdRequired'), trigger: 'blur' }],
|
||||
areaId: [{ required: true, message: t('ErpStock.WarehouseLocation.validatorAreaIdRequired'), trigger: 'blur' }],
|
||||
name: [{ required: true, message: t('ErpStock.WarehouseLocation.validatorNameRequired'), trigger: 'blur' }],
|
||||
allowProductMix: [{ required: true, message: t('ErpStock.WarehouseLocation.validatorAllowProductMixRequired'), trigger: 'blur' }],
|
||||
allowBatchMix: [{ required: true, message: t('ErpStock.WarehouseLocation.validatorAllowBatchMixRequired'), trigger: 'blur' }],
|
||||
status: [{ required: true, message: t('ErpStock.WarehouseLocation.validatorStatusRequired'), trigger: 'blur' }]
|
||||
})
|
||||
const formRef = ref()
|
||||
const warehouseList = ref<any[]>([])
|
||||
const areaList = ref<any[]>([])
|
||||
|
||||
const filteredAreaList = computed(() => {
|
||||
if (!formData.value.warehouseId) return areaList.value
|
||||
return areaList.value.filter((item) => item.warehouseId === formData.value.warehouseId)
|
||||
})
|
||||
|
||||
const handleWarehouseChange = () => {
|
||||
formData.value.areaId = undefined
|
||||
}
|
||||
|
||||
const open = async (type: string, id?: number) => {
|
||||
dialogVisible.value = true
|
||||
dialogTitle.value = t('action.' + type)
|
||||
formType.value = type
|
||||
resetForm()
|
||||
warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
|
||||
const areaData = await WarehouseAreaApi.getWarehouseAreaPage({ pageNo: 1, pageSize: 100 })
|
||||
areaList.value = areaData.list ?? []
|
||||
if (id) {
|
||||
formLoading.value = true
|
||||
try {
|
||||
formData.value = await WarehouseLocationApi.getWarehouseLocation(id)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
defineExpose({ open })
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
const submitForm = async () => {
|
||||
await formRef.value.validate()
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = { ...formData.value } as unknown as WarehouseLocationVO
|
||||
if (data.isCode) {
|
||||
data.code = undefined
|
||||
}
|
||||
if (formType.value === 'create') {
|
||||
await WarehouseLocationApi.createWarehouseLocation(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await WarehouseLocationApi.updateWarehouseLocation(data)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
warehouseId: undefined,
|
||||
areaId: undefined,
|
||||
code: undefined,
|
||||
name: undefined,
|
||||
areaSize: undefined,
|
||||
maxLoadWeight: undefined,
|
||||
positionX: undefined,
|
||||
positionY: undefined,
|
||||
positionZ: undefined,
|
||||
allowProductMix: false,
|
||||
allowBatchMix: false,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
isCode: true
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
@ -1,275 +0,0 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="auto"
|
||||
>
|
||||
<el-form-item :label="t('ErpStock.WarehouseLocation.warehouseId')" prop="warehouseId">
|
||||
<el-select
|
||||
v-model="queryParams.warehouseId"
|
||||
:placeholder="t('ErpStock.WarehouseLocation.placeholderWarehouseId')"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
@change="handleWarehouseChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in warehouseList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseLocation.areaId')" prop="areaId">
|
||||
<el-select
|
||||
v-model="queryParams.areaId"
|
||||
:placeholder="t('ErpStock.WarehouseLocation.placeholderAreaId')"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in filteredAreaList"
|
||||
:key="item.id"
|
||||
:label="item.areaName"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseLocation.code')" prop="code">
|
||||
<el-input
|
||||
v-model="queryParams.code"
|
||||
:placeholder="t('ErpStock.WarehouseLocation.placeholderCode')"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseLocation.name')" prop="name" v-show="showAllFilters">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
:placeholder="t('ErpStock.WarehouseLocation.placeholderName')"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('ErpStock.WarehouseLocation.status')" prop="status" v-show="showAllFilters">
|
||||
<el-select
|
||||
v-model="queryParams.status"
|
||||
:placeholder="t('ErpStock.WarehouseLocation.placeholderStatus')"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="filterCount > 3">
|
||||
<el-button type="text" class="text-primary" @click="toggleFilters">
|
||||
<Icon :icon="showAllFilters ? 'ep:arrow-up' : 'ep:arrow-down'" class="mr-5px" />
|
||||
{{ showAllFilters ? t('FactoryModeling.FactoryStructure.collapseText') :
|
||||
t('FactoryModeling.FactoryStructure.expandText') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> {{ t('common.query') }}</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> {{ t('common.reset') }}</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="openForm('create')"
|
||||
v-hasPermi="['erp:warehouse-location:create']"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> {{ t('action.add') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<ContentWrap>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
:stripe="true"
|
||||
:show-overflow-tooltip="true"
|
||||
row-key="id"
|
||||
>
|
||||
<el-table-column :label="t('ErpStock.WarehouseLocation.warehouseId')" align="center" prop="warehouseId" sortable>
|
||||
<template #default="scope">
|
||||
{{ getWarehouseName(scope.row.warehouseId) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('ErpStock.WarehouseLocation.areaId')" align="center" prop="areaId" sortable>
|
||||
<template #default="scope">
|
||||
{{ getAreaName(scope.row.areaId) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('ErpStock.WarehouseLocation.code')" align="center" prop="code" sortable />
|
||||
<el-table-column :label="t('ErpStock.WarehouseLocation.name')" align="center" prop="name" sortable />
|
||||
<el-table-column :label="t('ErpStock.WarehouseLocation.areaSize')" align="center" prop="areaSize" />
|
||||
<el-table-column :label="t('ErpStock.WarehouseLocation.maxLoadWeight')" align="center" prop="maxLoadWeight" />
|
||||
<el-table-column :label="t('ErpStock.WarehouseLocation.position')" align="center">
|
||||
<template #default="scope">
|
||||
{{ scope.row.positionX ?? '-' }}, {{ scope.row.positionY ?? '-' }}, {{ scope.row.positionZ ?? '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('ErpStock.WarehouseLocation.allowProductMix')" align="center" prop="allowProductMix">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.allowProductMix ? 'success' : 'danger'" size="small">
|
||||
{{ scope.row.allowProductMix ? t('common.yes') : t('common.no') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('ErpStock.WarehouseLocation.allowBatchMix')" align="center" prop="allowBatchMix">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.allowBatchMix ? 'success' : 'danger'" size="small">
|
||||
{{ scope.row.allowBatchMix ? t('common.yes') : t('common.no') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('ErpStock.WarehouseLocation.status')" align="center" prop="status" sortable>
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('ErpStock.WarehouseLocation.createTime')" align="center" prop="createTime" :formatter="dateFormatter" width="180px" sortable />
|
||||
<el-table-column :label="t('common.operate')" align="center" width="150px">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
v-hasPermi="['erp:warehouse-location:update']"
|
||||
>
|
||||
{{ t('action.edit') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['erp:warehouse-location:delete']"
|
||||
>
|
||||
{{ t('action.del') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<WarehouseLocationForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { WarehouseLocationApi, WarehouseLocationVO } from '@/api/erp/stock/warehouselocation'
|
||||
import { WarehouseApi } from '@/api/erp/stock/warehouse'
|
||||
import { WarehouseAreaApi } from '@/api/erp/stock/warehousearea'
|
||||
import WarehouseLocationForm from './WarehouseLocationForm.vue'
|
||||
|
||||
defineOptions({ name: 'ErpWarehouseLocation' })
|
||||
|
||||
const message = useMessage()
|
||||
const { t } = useI18n()
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref<WarehouseLocationVO[]>([])
|
||||
const total = ref(0)
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
warehouseId: undefined,
|
||||
areaId: undefined,
|
||||
code: undefined,
|
||||
name: undefined,
|
||||
status: undefined
|
||||
})
|
||||
const queryFormRef = ref()
|
||||
const showAllFilters = ref(false)
|
||||
const filterCount = 5
|
||||
const toggleFilters = () => {
|
||||
showAllFilters.value = !showAllFilters.value
|
||||
}
|
||||
const warehouseList = ref<any[]>([])
|
||||
const areaList = ref<any[]>([])
|
||||
|
||||
const filteredAreaList = computed(() => {
|
||||
if (!queryParams.warehouseId) return areaList.value
|
||||
return areaList.value.filter((item) => item.warehouseId === queryParams.warehouseId)
|
||||
})
|
||||
|
||||
const getWarehouseName = (warehouseId: number) => {
|
||||
return warehouseList.value.find((w) => w.id === warehouseId)?.name ?? '-'
|
||||
}
|
||||
|
||||
const getAreaName = (areaId: number) => {
|
||||
return areaList.value.find((a) => a.id === areaId)?.areaName ?? '-'
|
||||
}
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await WarehouseLocationApi.getWarehouseLocationPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getWarehouseList = async () => {
|
||||
warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
|
||||
}
|
||||
|
||||
const getAreaList = async () => {
|
||||
const data = await WarehouseAreaApi.getWarehouseAreaPage({ pageNo: 1, pageSize: 100 })
|
||||
areaList.value = data.list ?? []
|
||||
}
|
||||
|
||||
const handleWarehouseChange = () => {
|
||||
queryParams.areaId = undefined
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await message.delConfirm()
|
||||
await WarehouseLocationApi.deleteWarehouseLocation(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getWarehouseList()
|
||||
getAreaList()
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,277 +0,0 @@
|
||||
<template>
|
||||
<ContentWrap class="timeline-panel">
|
||||
<div class="timeline-panel__header">
|
||||
<div class="timeline-panel__title">{{ t('DataCollection.RunOverview.timelineTitle') }}</div>
|
||||
<div class="timeline-panel__actions">
|
||||
<el-select model-value="" class="!w-140px">
|
||||
<el-option value="" :label="t('DataCollection.RunOverview.deviceFilterAll')" />
|
||||
</el-select>
|
||||
<el-select model-value="expand" class="!w-120px">
|
||||
<el-option value="expand" :label="t('DataCollection.RunOverview.expandAllText')" />
|
||||
</el-select>
|
||||
<el-button class="timeline-panel__icon-btn">
|
||||
<Icon icon="ep:data-analysis" />
|
||||
</el-button>
|
||||
<el-button class="timeline-panel__icon-btn">
|
||||
<Icon icon="ep:download" />
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline-legend">
|
||||
<div v-for="item in legendItems" :key="item.status" class="timeline-legend__item">
|
||||
<span class="timeline-legend__dot" :style="{ background: item.color }"></span>
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline-table">
|
||||
<div class="timeline-table__head">
|
||||
<div class="timeline-table__meta">
|
||||
<div class="timeline-table__device">{{ t('DataCollection.RunOverview.tableDeviceNameColumn') }}</div>
|
||||
<div class="timeline-table__rate">{{ t('DataCollection.RunOverview.tableUtilizationRateColumn') }}</div>
|
||||
</div>
|
||||
<div class="timeline-scale">
|
||||
<div v-for="hour in hourTicks" :key="hour" class="timeline-scale__tick">
|
||||
{{ hour }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-for="row in timelineRows" :key="row.id" class="timeline-table__row">
|
||||
<div class="timeline-table__meta">
|
||||
<div class="timeline-table__device">{{ row.name }}</div>
|
||||
<div class="timeline-table__rate">{{ row.utilizationRate.toFixed(2) }}%</div>
|
||||
</div>
|
||||
<div class="timeline-track">
|
||||
<div v-for="hour in 24" :key="hour" class="timeline-track__hour"></div>
|
||||
<div
|
||||
v-for="(segment, index) in row.segments"
|
||||
:key="`${row.id}-${index}`"
|
||||
class="timeline-segment"
|
||||
:style="{
|
||||
left: `${(segment.startHour / 24) * 100}%`,
|
||||
width: `${((segment.endHour - segment.startHour) / 24) * 100}%`,
|
||||
background: statusColors[segment.status]
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline-panel__footer">
|
||||
<div class="timeline-panel__summary">
|
||||
{{ t('DataCollection.RunOverview.statisticsTimeText', { start: statisticsStart, end: statisticsEnd }) }}
|
||||
</div>
|
||||
<div class="timeline-panel__pager">
|
||||
<span>{{ t('DataCollection.RunOverview.totalDevicesText', { total }) }}</span>
|
||||
<el-select :model-value="pageSize" class="!w-100px" @update:model-value="emit('update:pageSize', Number($event))">
|
||||
<el-option :value="10" :label="`10${t('DataCollection.RunOverview.pageUnit')}`" />
|
||||
<el-option :value="20" :label="`20${t('DataCollection.RunOverview.pageUnit')}`" />
|
||||
</el-select>
|
||||
<el-pagination
|
||||
layout="prev, pager, next"
|
||||
:page-size="pageSize"
|
||||
:current-page="pageNo"
|
||||
:total="total"
|
||||
small
|
||||
@current-change="emit('update:pageNo', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { DeviceTimelineRow, RunStatus } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
rows: DeviceTimelineRow[]
|
||||
total: number
|
||||
pageNo: number
|
||||
pageSize: number
|
||||
statisticsStart: string
|
||||
statisticsEnd: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:pageNo', value: number): void
|
||||
(e: 'update:pageSize', value: number): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const statusColors: Record<RunStatus, string> = {
|
||||
running: '#67c35b',
|
||||
standby: '#f5c243',
|
||||
fault: '#ff6b6b',
|
||||
offline: '#d5dae3'
|
||||
}
|
||||
|
||||
const legendItems = computed(() => [
|
||||
{ status: 'running' as const, color: statusColors.running, label: t('DataCollection.RunOverview.legend.running') },
|
||||
{ status: 'standby' as const, color: statusColors.standby, label: t('DataCollection.RunOverview.legend.standby') },
|
||||
{ status: 'fault' as const, color: statusColors.fault, label: t('DataCollection.RunOverview.legend.fault') },
|
||||
{ status: 'offline' as const, color: statusColors.offline, label: t('DataCollection.RunOverview.legend.offline') }
|
||||
])
|
||||
|
||||
const hourTicks = Array.from({ length: 24 }, (_, index) => `${String(index).padStart(2, '0')}:00`)
|
||||
|
||||
const timelineRows = computed(() =>
|
||||
props.rows.map((row) => ({
|
||||
...row,
|
||||
segments: (row.segments || []).filter(
|
||||
(segment) =>
|
||||
!!statusColors[segment.status] &&
|
||||
Number.isFinite(segment.startHour) &&
|
||||
Number.isFinite(segment.endHour) &&
|
||||
segment.endHour > segment.startHour
|
||||
)
|
||||
}))
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.timeline-panel__header,
|
||||
.timeline-panel__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.timeline-panel__title {
|
||||
color: #101828;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.timeline-panel__actions,
|
||||
.timeline-panel__pager {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.timeline-panel__icon-btn {
|
||||
width: 36px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.timeline-legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
margin: 10px 0 18px;
|
||||
}
|
||||
|
||||
.timeline-legend__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #475467;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.timeline-legend__dot {
|
||||
width: 18px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.timeline-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.timeline-table__head,
|
||||
.timeline-table__row {
|
||||
display: grid;
|
||||
grid-template-columns: 210px minmax(720px, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.timeline-table__head {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.timeline-table__row {
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.timeline-table__meta {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 70px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.timeline-table__device,
|
||||
.timeline-table__rate {
|
||||
color: #344054;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.timeline-table__head .timeline-table__device,
|
||||
.timeline-table__head .timeline-table__rate {
|
||||
color: #475467;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.timeline-scale,
|
||||
.timeline-track {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(24, 1fr);
|
||||
}
|
||||
|
||||
.timeline-scale {
|
||||
color: #667085;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.timeline-scale__tick {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline-scale__tick::after {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 0;
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
background: #e4e7ec;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.timeline-track {
|
||||
height: 20px;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.timeline-track__hour {
|
||||
border-right: 1px solid #edf1f7;
|
||||
}
|
||||
|
||||
.timeline-segment {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.timeline-panel__summary {
|
||||
color: #667085;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.timeline-panel__header,
|
||||
.timeline-panel__footer {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,139 +0,0 @@
|
||||
<template>
|
||||
<ContentWrap class="run-overview-filter">
|
||||
<el-form :model="modelValue" inline label-width="auto">
|
||||
<el-form-item :label="t('DataCollection.RunOverview.groupLabel')">
|
||||
<el-tree-select
|
||||
:model-value="modelValue.groupId"
|
||||
:data="groupOptions"
|
||||
:props="treeProps"
|
||||
check-strictly
|
||||
clearable
|
||||
default-expand-all
|
||||
class="!w-220px"
|
||||
:placeholder="t('DataCollection.RunOverview.groupPlaceholder')"
|
||||
@update:model-value="handleGroupChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('DataCollection.RunOverview.deviceLabel')">
|
||||
<el-select
|
||||
:model-value="modelValue.deviceId"
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
clearable
|
||||
:placeholder="t('DataCollection.RunOverview.devicePlaceholder')"
|
||||
class="!w-240px"
|
||||
@update:model-value="updateField('deviceId', $event || [])"
|
||||
>
|
||||
<el-option v-for="item in deviceOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('DataCollection.RunOverview.timeRangeLabel')">
|
||||
<el-date-picker
|
||||
:model-value="modelValue.timeRange"
|
||||
type="datetimerange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
:start-placeholder="t('DataCollection.RunOverview.timeRangeStartPlaceholder')"
|
||||
:end-placeholder="t('DataCollection.RunOverview.timeRangeEndPlaceholder')"
|
||||
class="!w-360px"
|
||||
@update:model-value="updateField('timeRange', $event)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="run-overview-filter__actions">
|
||||
<el-button
|
||||
v-for="item in quickRanges"
|
||||
:key="item.value"
|
||||
:type="modelValue.quickRange === item.value ? 'primary' : 'default'"
|
||||
@click="emit('quick-range-change', item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="emit('query')">
|
||||
<Icon icon="ep:search" class="mr-5px" />
|
||||
{{ t('DataCollection.RunOverview.searchButtonText') }}
|
||||
</el-button>
|
||||
<el-button @click="emit('reset')">
|
||||
<Icon icon="ep:refresh" class="mr-5px" />
|
||||
{{ t('DataCollection.RunOverview.resetButtonText') }}
|
||||
</el-button>
|
||||
<!-- <el-button type="success" plain @click="emit('export')">
|
||||
<Icon icon="ep:download" class="mr-5px" />
|
||||
{{ t('DataCollection.RunOverview.exportButtonText') }}
|
||||
</el-button>-->
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
OrganizationTreeOption,
|
||||
OverviewOption,
|
||||
QuickRangeKey,
|
||||
RunOverviewQueryParams
|
||||
} from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: RunOverviewQueryParams
|
||||
groupOptions: OrganizationTreeOption[]
|
||||
deviceOptions: OverviewOption[]
|
||||
quickRanges: Array<{ label: string; value: QuickRangeKey }>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: RunOverviewQueryParams): void
|
||||
(e: 'query'): void
|
||||
(e: 'reset'): void
|
||||
(e: 'export'): void
|
||||
(e: 'quick-range-change', value: QuickRangeKey): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const treeProps = {
|
||||
value: 'id',
|
||||
label: 'name',
|
||||
children: 'children'
|
||||
}
|
||||
|
||||
const updateField = <K extends keyof RunOverviewQueryParams>(key: K, value: RunOverviewQueryParams[K]) => {
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
[key]: value
|
||||
})
|
||||
}
|
||||
|
||||
const handleGroupChange = (value: string | number | undefined) => {
|
||||
const nextGroupId = value == null ? '' : String(value)
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
groupId: nextGroupId,
|
||||
deviceId: props.modelValue.deviceId
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.run-overview-filter {
|
||||
:deep(.el-form) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px 0;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
min-width: 84px;
|
||||
color: #475467;
|
||||
}
|
||||
}
|
||||
|
||||
.run-overview-filter__actions :deep(.el-button + .el-button) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
@ -1,120 +0,0 @@
|
||||
<template>
|
||||
<div class="metric-grid">
|
||||
<div v-for="item in metrics" :key="item.key" class="metric-card">
|
||||
<div class="metric-card__icon" :style="{ background: itemColors[item.key]?.bg, color: itemColors[item.key]?.fg }">
|
||||
<Icon :icon="item.icon" />
|
||||
</div>
|
||||
<div class="metric-card__content">
|
||||
<div class="metric-card__title">{{ t(`DataCollection.RunOverview.metrics.${item.key}`) }}</div>
|
||||
<div class="metric-card__value">
|
||||
{{ item.value.toFixed(2) }}<span>{{ item.unit }}</span>
|
||||
</div>
|
||||
<div class="metric-card__compare">
|
||||
<span>{{ t('DataCollection.RunOverview.compareLabel') }}</span>
|
||||
<span :class="item.change >= 0 ? 'is-up' : 'is-down'">
|
||||
{{ item.change >= 0 ? '↑' : '↓' }} {{ Math.abs(item.change).toFixed(2) }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { RunOverviewMetric } from './types'
|
||||
|
||||
defineProps<{
|
||||
metrics: RunOverviewMetric[]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const itemColors: Record<string, { bg: string; fg: string }> = {
|
||||
utilizationRate: { bg: 'rgba(61, 132, 255, 0.12)', fg: '#3d84ff' },
|
||||
powerOnRate: { bg: 'rgba(86, 196, 94, 0.12)', fg: '#56c45e' },
|
||||
faultRate: { bg: 'rgba(255, 164, 54, 0.12)', fg: '#ffa436' },
|
||||
standbyRate: { bg: 'rgba(156, 108, 255, 0.12)', fg: '#9c6cff' }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 104px;
|
||||
padding: 22px 24px;
|
||||
background: #fff;
|
||||
border: 1px solid #edf1f7;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.metric-card__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
margin-right: 18px;
|
||||
font-size: 28px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.metric-card__content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.metric-card__title {
|
||||
margin-bottom: 6px;
|
||||
color: #475467;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.metric-card__value {
|
||||
color: #101828;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
span {
|
||||
margin-left: 2px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.metric-card__compare {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
color: #667085;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.is-up {
|
||||
color: #f04438;
|
||||
}
|
||||
|
||||
.is-down {
|
||||
color: #12b76a;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.metric-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.metric-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,271 +0,0 @@
|
||||
<template>
|
||||
<ContentWrap class="chart-panel">
|
||||
<div class="chart-panel__header">
|
||||
<div class="chart-panel__title">
|
||||
{{ t('DataCollection.RunOverview.statusDistributionTitle') }}
|
||||
</div>
|
||||
<div class="chart-panel__actions">
|
||||
<el-select model-value="hour" class="!w-120px">
|
||||
<el-option value="hour" :label="t('DataCollection.RunOverview.granularityHour')" />
|
||||
</el-select>
|
||||
<el-button class="chart-panel__icon-btn">
|
||||
<Icon icon="ep:data-analysis" />
|
||||
</el-button>
|
||||
<el-button class="chart-panel__icon-btn">
|
||||
<Icon icon="ep:download" />
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-panel__body">
|
||||
<div class="chart-panel__main">
|
||||
<Echart :options="barOption" height="320px" />
|
||||
</div>
|
||||
<div class="chart-panel__side">
|
||||
<div class="chart-panel__side-title">{{ t('DataCollection.RunOverview.summaryTitle') }}</div>
|
||||
<div class="chart-panel__summary-layout">
|
||||
<!-- <div class="chart-panel__pie">
|
||||
<Echart :options="pieOption" height="280px" />
|
||||
</div>–>-->
|
||||
<Echart :options="pieOption" height="280px" />
|
||||
<div class="chart-panel__legend">
|
||||
<div v-for="item in summary" :key="item.status" class="chart-panel__legend-item">
|
||||
<span class="dot" :style="{ background: statusColors[item.status] }"></span>
|
||||
<span class="label">{{ statusLabelMap[item.status] }}</span>
|
||||
<span class="value">{{ item.percent.toFixed(2) }}% ({{ item.hours.toFixed(2) }}h)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import { Echart as EchartChart } from '@/components/Echart'
|
||||
import type { HourlyStatusItem, RunStatus, StatusSummaryItem } from './types'
|
||||
|
||||
const Echart = EchartChart
|
||||
|
||||
const props = defineProps<{
|
||||
hourlyStatus: HourlyStatusItem[]
|
||||
summary: StatusSummaryItem[]
|
||||
summaryTotalHours?: number
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const statusColors: Record<RunStatus, string> = {
|
||||
running: '#67c35b',
|
||||
standby: '#f5c243',
|
||||
fault: '#ff6b6b',
|
||||
offline: '#cfd4df'
|
||||
}
|
||||
|
||||
const statusLabelMap = computed<Record<RunStatus, string>>(() => ({
|
||||
running: t('DataCollection.RunOverview.legend.running'),
|
||||
standby: t('DataCollection.RunOverview.legend.standby'),
|
||||
fault: t('DataCollection.RunOverview.legend.fault'),
|
||||
offline: t('DataCollection.RunOverview.legend.offline')
|
||||
}))
|
||||
|
||||
const barOption = computed<EChartsOption>(() => ({
|
||||
color: [
|
||||
statusColors.running,
|
||||
statusColors.standby,
|
||||
statusColors.fault,
|
||||
statusColors.offline
|
||||
],
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||
legend: {
|
||||
top: 8,
|
||||
itemWidth: 14,
|
||||
itemHeight: 8,
|
||||
textStyle: { color: '#667085', fontSize: 12 }
|
||||
},
|
||||
grid: { left: 48, right: 18, top: 46, bottom: 36 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: props.hourlyStatus.map((item) => item.hour),
|
||||
axisTick: { show: false },
|
||||
axisLine: { lineStyle: { color: '#d0d5dd' } },
|
||||
axisLabel: { color: '#667085', fontSize: 11 }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 100,
|
||||
interval: 20,
|
||||
axisLabel: { color: '#667085', formatter: '{value}%' },
|
||||
splitLine: { lineStyle: { color: '#eef2f6' } }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: statusLabelMap.value.running,
|
||||
type: 'bar',
|
||||
stack: 'status',
|
||||
barWidth: 18,
|
||||
data: props.hourlyStatus.map((item) => item.running)
|
||||
},
|
||||
{
|
||||
name: statusLabelMap.value.standby,
|
||||
type: 'bar',
|
||||
stack: 'status',
|
||||
barWidth: 18,
|
||||
data: props.hourlyStatus.map((item) => item.standby)
|
||||
},
|
||||
{
|
||||
name: statusLabelMap.value.fault,
|
||||
type: 'bar',
|
||||
stack: 'status',
|
||||
barWidth: 18,
|
||||
data: props.hourlyStatus.map((item) => item.fault)
|
||||
},
|
||||
{
|
||||
name: statusLabelMap.value.offline,
|
||||
type: 'bar',
|
||||
stack: 'status',
|
||||
barWidth: 18,
|
||||
data: props.hourlyStatus.map((item) => item.offline)
|
||||
}
|
||||
]
|
||||
}))
|
||||
|
||||
const pieOption = computed<EChartsOption>(() => ({
|
||||
color: props.summary.map((item) => statusColors[item.status]),
|
||||
tooltip: { trigger: 'item', formatter: '{b}: {c}%' },
|
||||
graphic: [
|
||||
{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: '42%',
|
||||
style: {
|
||||
text: `${t('DataCollection.RunOverview.totalTimeLabel')}\n${(props.summaryTotalHours ?? 0).toFixed(2)} h`,
|
||||
textAlign: 'center',
|
||||
fill: '#101828',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
lineHeight: 24
|
||||
}
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: t('DataCollection.RunOverview.summaryTitle'),
|
||||
type: 'pie',
|
||||
radius: ['55%', '76%'],
|
||||
center: ['50%', '50%'],
|
||||
label: { show: false },
|
||||
data: props.summary.map((item) => ({
|
||||
name: statusLabelMap.value[item.status],
|
||||
value: item.percent
|
||||
}))
|
||||
}
|
||||
]
|
||||
}))
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chart-panel {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-panel__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chart-panel__title,
|
||||
.chart-panel__side-title {
|
||||
color: #101828;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.chart-panel__actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-panel__icon-btn {
|
||||
width: 36px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.chart-panel__body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.7fr) minmax(280px, 0.8fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.chart-panel__side {
|
||||
padding: 8px 12px 0;
|
||||
}
|
||||
|
||||
.chart-panel__summary-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 220px) minmax(180px, 1fr);
|
||||
align-items: center;
|
||||
gap: 8px 16px;
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.chart-panel__pie {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chart-panel__legend {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chart-panel__legend-item {
|
||||
display: grid;
|
||||
grid-template-columns: 12px 52px 1fr;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
color: #344054;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #101828;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.chart-panel__body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.chart-panel__summary-layout {
|
||||
grid-template-columns: 1fr;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.chart-panel__legend {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,75 +0,0 @@
|
||||
export type RunStatus = 'running' | 'standby' | 'fault' | 'offline'
|
||||
|
||||
export type QuickRangeKey = 'today' | 'yesterday' | 'last7Days' | 'last30Days'
|
||||
|
||||
export interface OverviewOption {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface OrganizationFilterItem {
|
||||
id: number | string
|
||||
name: string
|
||||
parentId?: number | string | null
|
||||
dvId?: number | string | null
|
||||
machineName?: string
|
||||
}
|
||||
|
||||
export interface OrganizationTreeOption {
|
||||
id: number | string
|
||||
name: string
|
||||
parentId?: number | string | null
|
||||
dvId?: number | string | null
|
||||
children?: OrganizationTreeOption[]
|
||||
}
|
||||
|
||||
export interface RunOverviewQueryParams {
|
||||
groupId: string
|
||||
deviceId: string[]
|
||||
quickRange: QuickRangeKey
|
||||
timeRange: [string, string]
|
||||
}
|
||||
|
||||
export interface RunOverviewMetric {
|
||||
key: string
|
||||
icon: string
|
||||
value: number
|
||||
unit: string
|
||||
change: number
|
||||
}
|
||||
|
||||
export interface HourlyStatusItem {
|
||||
hour: string
|
||||
running: number
|
||||
standby: number
|
||||
fault: number
|
||||
offline: number
|
||||
}
|
||||
|
||||
export interface StatusSummaryItem {
|
||||
status: RunStatus
|
||||
percent: number
|
||||
hours: number
|
||||
}
|
||||
|
||||
export interface TimelineSegment {
|
||||
status: RunStatus
|
||||
startHour: number
|
||||
endHour: number
|
||||
}
|
||||
|
||||
export interface DeviceTimelineRow {
|
||||
id: string
|
||||
name: string
|
||||
utilizationRate: number
|
||||
segments: TimelineSegment[]
|
||||
}
|
||||
|
||||
export interface RunOverviewData {
|
||||
metrics: RunOverviewMetric[]
|
||||
hourlyStatus: HourlyStatusItem[]
|
||||
summary: StatusSummaryItem[]
|
||||
summaryTotalHours: number
|
||||
timelineRows: DeviceTimelineRow[]
|
||||
totalDevices: number
|
||||
}
|
||||
@ -1,312 +0,0 @@
|
||||
<template>
|
||||
<div ref="fullscreenTargetRef" class="run-overview-page" :class="{ 'is-fullscreen': isFullscreen }">
|
||||
<div class="run-overview-page__floating-tools">
|
||||
<el-tooltip
|
||||
:content="
|
||||
isFullscreen
|
||||
? t('DataCollection.RunOverview.exitFullscreen')
|
||||
: t('DataCollection.RunOverview.enterFullscreen')
|
||||
"
|
||||
>
|
||||
<button class="run-overview-page__screenfull" type="button" @click="toggleFullscreen">
|
||||
<Icon :icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'" color="#344054" />
|
||||
</button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<OverviewFilterBar
|
||||
v-model="queryParams"
|
||||
:group-options="groupOptions"
|
||||
:device-options="deviceOptions"
|
||||
:quick-ranges="quickRanges"
|
||||
@quick-range-change="handleQuickRangeChange"
|
||||
@query="handleQuery"
|
||||
@reset="resetQuery"
|
||||
/>
|
||||
|
||||
<OverviewMetricCards :metrics="overviewData.metrics" />
|
||||
|
||||
<StatusDistributionChart
|
||||
:hourly-status="overviewData.hourlyStatus"
|
||||
:summary="overviewData.summary"
|
||||
:summary-total-hours="overviewData.summaryTotalHours"
|
||||
/>
|
||||
|
||||
<OperationTimelineChart
|
||||
:rows="overviewData.timelineRows"
|
||||
:total="overviewData.totalDevices"
|
||||
:page-no="pageNo"
|
||||
:page-size="pageSize"
|
||||
:statistics-start="queryParams.timeRange[0]"
|
||||
:statistics-end="queryParams.timeRange[1]"
|
||||
@update:page-no="pageNo = $event"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import { DeviceOperationOverviewApi } from '@/api/iot/deviceOperationOverview'
|
||||
import { OrganizationApi } from '@/api/mes/organization'
|
||||
import { handleTree } from '@/utils/tree'
|
||||
import OverviewFilterBar from './components/OverviewFilterBar.vue'
|
||||
import OverviewMetricCards from './components/OverviewMetricCards.vue'
|
||||
import OperationTimelineChart from './components/OperationTimelineChart.vue'
|
||||
import StatusDistributionChart from './components/StatusDistributionChart.vue'
|
||||
import { buildDefaultQueryParams } from './mock'
|
||||
import type {
|
||||
OrganizationFilterItem,
|
||||
OrganizationTreeOption,
|
||||
OverviewOption,
|
||||
QuickRangeKey,
|
||||
RunOverviewData,
|
||||
RunOverviewQueryParams
|
||||
} from './components/types'
|
||||
|
||||
defineOptions({ name: 'IotRunOverview' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const fullscreenTargetRef = ref()
|
||||
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen(fullscreenTargetRef)
|
||||
const organizationList = ref<OrganizationFilterItem[]>([])
|
||||
const groupOptions = ref<OrganizationTreeOption[]>([])
|
||||
|
||||
const createDateRangeByQuickKey = (key: QuickRangeKey): [string, string] => {
|
||||
const format = 'YYYY-MM-DD HH:mm:ss'
|
||||
if (key === 'today') return [dayjs().startOf('day').format(format), dayjs().endOf('day').format(format)]
|
||||
if (key === 'yesterday') {
|
||||
return [
|
||||
dayjs().subtract(1, 'day').startOf('day').format(format),
|
||||
dayjs().subtract(1, 'day').endOf('day').format(format)
|
||||
]
|
||||
}
|
||||
if (key === 'last7Days') return [dayjs().subtract(6, 'day').startOf('day').format(format), dayjs().endOf('day').format(format)]
|
||||
if (key === 'last30Days') {
|
||||
return [dayjs().subtract(29, 'day').startOf('day').format(format), dayjs().endOf('day').format(format)]
|
||||
}
|
||||
return [dayjs().startOf('day').format(format), dayjs().endOf('day').format(format)]
|
||||
}
|
||||
|
||||
const quickRanges = computed(() => [
|
||||
{ label: t('DataCollection.RunOverview.quickRange.today'), value: 'today' as const },
|
||||
{ label: t('DataCollection.RunOverview.quickRange.yesterday'), value: 'yesterday' as const },
|
||||
{ label: t('DataCollection.RunOverview.quickRange.last7Days'), value: 'last7Days' as const },
|
||||
{ label: t('DataCollection.RunOverview.quickRange.last30Days'), value: 'last30Days' as const },
|
||||
/* { label: t('DataCollection.RunOverview.quickRange.custom'), value: 'custom' as const }*/
|
||||
])
|
||||
|
||||
const queryParams = ref<RunOverviewQueryParams>(buildDefaultQueryParams())
|
||||
const pageNo = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
const organizationMap = computed(() => {
|
||||
return organizationList.value.reduce<Record<string, OrganizationFilterItem>>((acc, item) => {
|
||||
acc[String(item.id)] = item
|
||||
return acc
|
||||
}, {})
|
||||
})
|
||||
|
||||
const childrenByParentId = computed(() => {
|
||||
return organizationList.value.reduce<Record<string, OrganizationFilterItem[]>>((acc, item) => {
|
||||
const parentKey = String(item.parentId ?? 0)
|
||||
if (!acc[parentKey]) acc[parentKey] = []
|
||||
acc[parentKey].push(item)
|
||||
return acc
|
||||
}, {})
|
||||
})
|
||||
|
||||
const collectDeviceOptionsByGroup = (groupId?: string) => {
|
||||
const deviceMap = new Map<string, OverviewOption>()
|
||||
const pushDevice = (node?: OrganizationFilterItem) => {
|
||||
if (!node?.dvId || !node.machineName?.trim()) return
|
||||
const deviceId = String(node.dvId)
|
||||
if (!deviceMap.has(deviceId)) {
|
||||
deviceMap.set(deviceId, {
|
||||
label: node.machineName.trim(),
|
||||
value: deviceId
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!groupId) {
|
||||
organizationList.value.forEach(pushDevice)
|
||||
return Array.from(deviceMap.values())
|
||||
}
|
||||
|
||||
const queue = [groupId]
|
||||
while (queue.length > 0) {
|
||||
const currentId = queue.shift() as string
|
||||
pushDevice(organizationMap.value[currentId])
|
||||
const children = childrenByParentId.value[currentId] || []
|
||||
children.forEach((child) => queue.push(String(child.id)))
|
||||
}
|
||||
|
||||
return Array.from(deviceMap.values())
|
||||
}
|
||||
|
||||
const deviceOptions = computed(() => collectDeviceOptionsByGroup(queryParams.value.groupId))
|
||||
const overviewData = ref<RunOverviewData>({
|
||||
metrics: [],
|
||||
hourlyStatus: [],
|
||||
summary: [],
|
||||
summaryTotalHours: 0,
|
||||
timelineRows: [],
|
||||
totalDevices: 0
|
||||
})
|
||||
|
||||
const currentDeviceIds = computed(() =>
|
||||
queryParams.value.deviceId.length > 0 ? queryParams.value.deviceId : deviceOptions.value.map((item) => item.value)
|
||||
)
|
||||
|
||||
const buildOverviewRequestParams = () => ({
|
||||
ids: currentDeviceIds.value.join(','),
|
||||
startTime: queryParams.value.timeRange[0],
|
||||
endTime: queryParams.value.timeRange[1],
|
||||
timelinePageNo: pageNo.value,
|
||||
timelinePageSize: pageSize.value
|
||||
})
|
||||
|
||||
const refreshData = async () => {
|
||||
const response = await DeviceOperationOverviewApi.getRunOverview(buildOverviewRequestParams())
|
||||
overviewData.value = {
|
||||
metrics: response?.metrics || [],
|
||||
hourlyStatus: response?.hourlyStatus || [],
|
||||
summary: response?.summary || [],
|
||||
summaryTotalHours: response?.summaryTotalHours || 0,
|
||||
timelineRows: response?.timelineRows || [],
|
||||
totalDevices: response?.totalDevices || 0
|
||||
}
|
||||
const maxPage = Math.max(1, Math.ceil(overviewData.value.totalDevices / pageSize.value))
|
||||
if (pageNo.value > maxPage) pageNo.value = maxPage
|
||||
}
|
||||
|
||||
const resetToFirstPageAndRefresh = () => {
|
||||
if (pageNo.value !== 1) {
|
||||
pageNo.value = 1
|
||||
return
|
||||
}
|
||||
void refreshData()
|
||||
}
|
||||
|
||||
const handleQuickRangeChange = (key: QuickRangeKey) => {
|
||||
queryParams.value = {
|
||||
...queryParams.value,
|
||||
quickRange: key,
|
||||
timeRange: createDateRangeByQuickKey(key)
|
||||
}
|
||||
pageNo.value = 1
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
resetToFirstPageAndRefresh()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryParams.value = buildDefaultQueryParams()
|
||||
pageSize.value = 10
|
||||
resetToFirstPageAndRefresh()
|
||||
}
|
||||
|
||||
const handleExport = () => {
|
||||
message.success(t('DataCollection.RunOverview.exportPlaceholderMessage'))
|
||||
}
|
||||
|
||||
const handlePageSizeChange = (size: number) => {
|
||||
pageSize.value = size
|
||||
pageNo.value = 1
|
||||
void refreshData()
|
||||
}
|
||||
|
||||
watch(pageNo, () => {
|
||||
void refreshData()
|
||||
})
|
||||
|
||||
const getOrganizationOptions = async () => {
|
||||
const data = await OrganizationApi.getOrganizationList()
|
||||
organizationList.value = Array.isArray(data)
|
||||
? data.map((item) => ({
|
||||
id: String(item.id),
|
||||
name: item.name,
|
||||
parentId: item.parentId != null ? String(item.parentId) : item.parentId,
|
||||
dvId: item.dvId,
|
||||
machineName: item.machineName
|
||||
}))
|
||||
: []
|
||||
groupOptions.value = handleTree(
|
||||
organizationList.value.map((item) => ({ ...item })),
|
||||
'id',
|
||||
'parentId'
|
||||
) as OrganizationTreeOption[]
|
||||
}
|
||||
|
||||
watch(
|
||||
deviceOptions,
|
||||
(options) => {
|
||||
if (queryParams.value.deviceId.length === 0) return
|
||||
const validDeviceIds = queryParams.value.deviceId.filter((deviceId) =>
|
||||
options.some((item) => item.value === deviceId)
|
||||
)
|
||||
if (validDeviceIds.length !== queryParams.value.deviceId.length) {
|
||||
queryParams.value = {
|
||||
...queryParams.value,
|
||||
deviceId: validDeviceIds
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await getOrganizationOptions()
|
||||
await refreshData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.run-overview-page {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
//padding-top: 8px;
|
||||
}
|
||||
|
||||
.run-overview-page.is-fullscreen {
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.run-overview-page__floating-tools {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 12px;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.run-overview-page__screenfull {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
border: 1px solid #e4e7ec;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.run-overview-page__screenfull:hover {
|
||||
background: #f8fbff;
|
||||
border-color: #bfd3ff;
|
||||
box-shadow: 0 8px 18px rgba(61, 132, 255, 0.12);
|
||||
}
|
||||
</style>
|
||||
@ -1,15 +0,0 @@
|
||||
import dayjs from 'dayjs'
|
||||
import type { RunOverviewQueryParams } from './components/types'
|
||||
|
||||
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
|
||||
|
||||
export const buildDefaultQueryParams = (): RunOverviewQueryParams => {
|
||||
const start = dayjs().startOf('day')
|
||||
const end = dayjs().endOf('day')
|
||||
return {
|
||||
groupId: '',
|
||||
deviceId: [],
|
||||
quickRange: 'today',
|
||||
timeRange: [start.format(DATE_TIME_FORMAT), end.format(DATE_TIME_FORMAT)]
|
||||
}
|
||||
}
|
||||
@ -1,174 +0,0 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" :title="dialogTitle" width="1300" :scroll="true" max-height="70vh" align-center>
|
||||
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="auto">
|
||||
<el-form-item :label="t('EquipmentManagement.CapacityReport.dialogProductCode')" prop="productCode">
|
||||
<el-input
|
||||
v-model="queryParams.productCode"
|
||||
:placeholder="t('EquipmentManagement.CapacityReport.dialogProductCodePlaceholder')"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('EquipmentManagement.CapacityReport.dialogProductName')" prop="productName">
|
||||
<el-input
|
||||
v-model="queryParams.productName"
|
||||
:placeholder="t('EquipmentManagement.CapacityReport.dialogProductNamePlaceholder')"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('EquipmentManagement.CapacityReport.dialogBaogongTime')" prop="baogongTime">
|
||||
<el-date-picker v-model="baogongTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange"
|
||||
:start-placeholder="t('EquipmentManagement.CapacityReport.dialogBaogongTimeStart')"
|
||||
:end-placeholder="t('EquipmentManagement.CapacityReport.dialogBaogongTimeEnd')"
|
||||
:shortcuts="baogongTimeShortcuts"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-240px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery">
|
||||
<Icon icon="ep:search" class="mr-5px" />
|
||||
{{ t('EquipmentManagement.CapacityReport.dialogSearchButtonText') }}
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">
|
||||
<Icon icon="ep:refresh" class="mr-5px" />
|
||||
{{ t('EquipmentManagement.CapacityReport.dialogResetButtonText') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" style="margin-top: 16px">
|
||||
<el-table-column :label="t('EquipmentManagement.CapacityReport.dialogProductCode')" align="center" prop="productCode" />
|
||||
<el-table-column :label="t('EquipmentManagement.CapacityReport.dialogProductName')" align="center" prop="productName" />
|
||||
<el-table-column :label="t('EquipmentManagement.CapacityReport.dialogBaogongTotal')" align="center" prop="baogongTotal" />
|
||||
<el-table-column :label="t('EquipmentManagement.CapacityReport.dialogAvgCapacity')" align="center" prop="avgCapacity" />
|
||||
</el-table>
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PlanApi } from '@/api/mes/plan'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
deviceId?: number
|
||||
deviceName?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const dialogVisible = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value: boolean) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
const name = props.deviceName || '-'
|
||||
return `${t('EquipmentManagement.CapacityReport.dialogTitlePrefix')}${name}`
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
deviceId: undefined as number | undefined,
|
||||
productCode: undefined as string | undefined,
|
||||
productName: undefined as string | undefined,
|
||||
beginBaogongTime: undefined as string | undefined,
|
||||
endBaogongTime: undefined as string | undefined
|
||||
})
|
||||
const baogongTime = ref<[string, string]>(['', ''])
|
||||
const baogongTimeShortcuts = [
|
||||
{
|
||||
text: t('EquipmentManagement.CapacityReport.shortcutLastWeek'),
|
||||
value: () => {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setDate(start.getDate() - 7)
|
||||
return [start, end]
|
||||
}
|
||||
},
|
||||
{
|
||||
text: t('EquipmentManagement.CapacityReport.shortcutLastHalfYear'),
|
||||
value: () => {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setMonth(start.getMonth() - 6)
|
||||
return [start, end]
|
||||
}
|
||||
},
|
||||
{
|
||||
text: t('EquipmentManagement.CapacityReport.shortcutLastYear'),
|
||||
value: () => {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setFullYear(start.getFullYear() - 1)
|
||||
return [start, end]
|
||||
}
|
||||
}
|
||||
]
|
||||
const queryFormRef = ref()
|
||||
|
||||
const getList = async () => {
|
||||
if (!props.deviceId) {
|
||||
list.value = []
|
||||
total.value = 0
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
...queryParams,
|
||||
beginBaogongTime: baogongTime.value?.[0] || undefined,
|
||||
endBaogongTime: baogongTime.value?.[1] || undefined
|
||||
}
|
||||
const data = await PlanApi.getProductCapacityPage(params)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields()
|
||||
baogongTime.value = ['', '']
|
||||
queryParams.deviceId = props.deviceId
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.deviceId],
|
||||
([visible, deviceId]) => {
|
||||
if (!visible) return
|
||||
queryParams.deviceId = deviceId as number
|
||||
queryParams.pageNo = 1
|
||||
queryParams.productCode = undefined
|
||||
queryParams.productName = undefined
|
||||
queryParams.taskCode = undefined
|
||||
getList()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@ -1,234 +0,0 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" min-label-width="80px">
|
||||
<el-form-item :label="t('EquipmentManagement.CapacityReport.deviceCode')" prop="deviceCode">
|
||||
<el-input v-model="queryParams.deviceCode"
|
||||
:placeholder="t('EquipmentManagement.CapacityReport.placeholderDeviceCode')" clearable
|
||||
@keyup.enter="handleQuery" class="!w-240px" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('EquipmentManagement.CapacityReport.deviceName')" prop="deviceName">
|
||||
<el-input v-model="queryParams.deviceName"
|
||||
:placeholder="t('EquipmentManagement.CapacityReport.placeholderDeviceName')" clearable
|
||||
@keyup.enter="handleQuery" class="!w-240px" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('EquipmentManagement.CapacityReport.baogongTime')" prop="baogongTime">
|
||||
<el-date-picker v-model="baogongTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange"
|
||||
:start-placeholder="t('EquipmentManagement.CapacityReport.placeholderBaogongTimeStart')"
|
||||
:end-placeholder="t('EquipmentManagement.CapacityReport.placeholderBaogongTimeEnd')"
|
||||
:shortcuts="baogongTimeShortcuts"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-240px" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="filterCount > 3">
|
||||
<el-button type="text" class="text-primary" @click="toggleFilters">
|
||||
<Icon :icon="showAllFilters ? 'ep:arrow-up' : 'ep:arrow-down'" class="mr-5px" />
|
||||
{{ showAllFilters ? t('FactoryModeling.FactoryStructure.collapseText') :
|
||||
t('FactoryModeling.FactoryStructure.expandText') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery">
|
||||
<Icon icon="ep:search" class="mr-5px" /> {{ t('common.query') }}
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">
|
||||
<Icon icon="ep:refresh" class="mr-5px" /> {{ t('common.reset') }}
|
||||
</el-button>
|
||||
<el-button type="success" plain @click="handleExport" :loading="exportLoading">
|
||||
<Icon icon="ep:download" class="mr-5px" /> {{ t('EquipmentManagement.EquipmentLedger.export') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" :row-key="(row) => row.id"
|
||||
@select-all="handleSelectAll" @select="handleSelect">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column :label="t('EquipmentManagement.CapacityReport.deviceCode')" align="center" prop="deviceCode"
|
||||
sortable />
|
||||
<el-table-column :label="t('EquipmentManagement.CapacityReport.deviceName')" align="center" prop="deviceName"
|
||||
sortable />
|
||||
<el-table-column :label="t('EquipmentManagement.CapacityReport.deviceType')" align="center" prop="typeName"
|
||||
sortable />
|
||||
<el-table-column :label="t('EquipmentManagement.CapacityReport.ratedCapacity')" align="center"
|
||||
prop="ratedCapacity" sortable />
|
||||
<el-table-column align="center" prop="reportCapacity">
|
||||
<template #header>
|
||||
<span>{{ t('EquipmentManagement.CapacityReport.reportCapacity') }}</span>
|
||||
<el-tooltip :content="t('EquipmentManagement.CapacityReport.reportCapacityTooltip')" placement="top">
|
||||
<Icon icon="ep:question-filled" class="ml-4px"
|
||||
style="vertical-align: middle; color: var(--el-text-color-secondary)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" @click="openProductCapacity(scope.row)">
|
||||
{{ t('EquipmentManagement.CapacityReport.reportCapacityViewDetail') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" prop="actualCapacity" sortable>
|
||||
<template #header>
|
||||
<span>{{ t('EquipmentManagement.CapacityReport.actualCapacity') }}</span>
|
||||
<el-tooltip :content="t('EquipmentManagement.CapacityReport.actualCapacityTooltip')" placement="top">
|
||||
<Icon icon="ep:question-filled" class="ml-4px"
|
||||
style="vertical-align: middle; color: var(--el-text-color-secondary)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('EquipmentManagement.CapacityReport.workshop')" align="center" prop="workshopName"
|
||||
sortable />
|
||||
</el-table>
|
||||
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList" />
|
||||
</ContentWrap>
|
||||
|
||||
<ProductCapacityDialog v-model="productCapacityVisible" :device-id="productCapacityDeviceId"
|
||||
:device-name="productCapacityDeviceName" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import download from '@/utils/download'
|
||||
import { DeviceLedgerApi, CapacityReportVO } from '@/api/mes/device-ledger'
|
||||
import ProductCapacityDialog from './ProductCapacityDialog.vue'
|
||||
|
||||
defineOptions({ name: 'CapacityReport' })
|
||||
|
||||
const message = useMessage()
|
||||
const { t } = useI18n()
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref<CapacityReportVO[]>([])
|
||||
const total = ref(0)
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
deviceCode: undefined,
|
||||
deviceName: undefined,
|
||||
deviceType: undefined,
|
||||
workshop: undefined,
|
||||
beginTime: undefined,
|
||||
endTime: undefined
|
||||
})
|
||||
const baogongTime = ref<[string, string]>(['', ''])
|
||||
const queryFormRef = ref()
|
||||
const exportLoading = ref(false)
|
||||
const showAllFilters = ref(false)
|
||||
const filterCount = 3
|
||||
const selectedIds = ref<number[]>([])
|
||||
const productCapacityVisible = ref(false)
|
||||
const productCapacityDeviceId = ref<number>()
|
||||
const productCapacityDeviceName = ref<string>()
|
||||
|
||||
const baogongTimeShortcuts = [
|
||||
{
|
||||
text: t('EquipmentManagement.CapacityReport.shortcutLastWeek'),
|
||||
value: () => {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
|
||||
return [start, end]
|
||||
}
|
||||
},
|
||||
{
|
||||
text: t('EquipmentManagement.CapacityReport.shortcutLastHalfYear'),
|
||||
value: () => {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setMonth(start.getMonth() - 6)
|
||||
return [start, end]
|
||||
}
|
||||
},
|
||||
{
|
||||
text: t('EquipmentManagement.CapacityReport.shortcutLastYear'),
|
||||
value: () => {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setFullYear(start.getFullYear() - 1)
|
||||
return [start, end]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const openProductCapacity = (row: CapacityReportVO) => {
|
||||
productCapacityDeviceId.value = row.id
|
||||
productCapacityDeviceName.value = row.deviceName
|
||||
productCapacityVisible.value = true
|
||||
}
|
||||
|
||||
/** 切换筛选框展开/折叠 */
|
||||
const toggleFilters = () => {
|
||||
showAllFilters.value = !showAllFilters.value
|
||||
}
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
...queryParams,
|
||||
beginTime: baogongTime.value?.[0] || undefined,
|
||||
endTime: baogongTime.value?.[1] || undefined
|
||||
}
|
||||
const data = await DeviceLedgerApi.getCapacityReportPage(params)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
selectedIds.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields()
|
||||
baogongTime.value = ['', '']
|
||||
selectedIds.value = []
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const handleSelectAll = (val: boolean) => {
|
||||
if (val) {
|
||||
selectedIds.value = list.value.map(item => item.id)
|
||||
} else {
|
||||
selectedIds.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelect = (val: boolean, row: CapacityReportVO) => {
|
||||
if (val) {
|
||||
selectedIds.value.push(row.id)
|
||||
} else {
|
||||
const index = selectedIds.value.indexOf(row.id)
|
||||
if (index > -1) {
|
||||
selectedIds.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
await message.exportConfirm()
|
||||
exportLoading.value = true
|
||||
const params: { ids?: string } = {}
|
||||
if (selectedIds.value.length > 0) {
|
||||
params.ids = selectedIds.value.join(',')
|
||||
}
|
||||
const data = await DeviceLedgerApi.exportCapacityReport(params)
|
||||
download.excel(data, t('EquipmentManagement.CapacityReport.exportFilename'))
|
||||
} catch {
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue