1个文件已删除
3个文件已添加
1 文件已重命名
9个文件已修改
| | |
| | | import { request } from '@/common/utils' |
| | | import type { TasksResponse } from './task' |
| | | import type { TasksGroupByChannel } from './task' |
| | | import type { PLCResponse } from './plc' |
| | | |
| | | export interface BaseResponse<T = any> { |
| | | code: number |
| | |
| | | msg: string |
| | | } |
| | | |
| | | export interface TaskListParams { |
| | | /** 1未完成2今天未完成3已完成 */ |
| | | type: 1 | 2 | 3 |
| | | /** 通道号 不传查所有通道的 */ |
| | | channel?: number |
| | | /** 从第几个开始查,从0开始 */ |
| | | offset: number |
| | | /** 查多少条 */ |
| | | limit: number |
| | | } |
| | | |
| | | /** |
| | | * 获取任务列表 |
| | | * @param taskMode 1: 待生产的任务 2: 看板各通道当前展示的任务 |
| | | * @param params |
| | | */ |
| | | export function getTaskList(taskMode: 1 | 2) { |
| | | return request<BaseResponse<TasksResponse>>({ |
| | | url: '/v1/task/get', |
| | | export function getTaskList(params: TaskListParams) { |
| | | return request<BaseResponse<TasksGroupByChannel>>({ |
| | | url: '/v1/task/list', |
| | | method: 'get', |
| | | params: { |
| | | taskMode |
| | | } |
| | | params |
| | | }) |
| | | } |
| | | |
| | | export interface ProductProgressParams { |
| | | channel: number |
| | | procedureId: number |
| | | } |
| | | |
| | | export function getProductProgress(params: ProductProgressParams) { |
| | | return request<BaseResponse<PLCResponse>>({ |
| | | url: '/v1/plc/productProgress', |
| | | method: 'post', |
| | | data: params |
| | | }) |
| | | } |
New file |
| | |
| | | export interface PLCResponse { |
| | | finishNumber: number |
| | | totalNumber: number |
| | | /** 1断开 2生产中 3待机*/ |
| | | plcStatus: 1 | 2 | 3 |
| | | } |
| | |
| | | workHours: string |
| | | inputMaterials: string |
| | | outputMaterials: string |
| | | workers: Workers[] |
| | | workers: Worker[] |
| | | allProcedureNames: string[] |
| | | channel: number |
| | | } |
| | |
| | | CanStarted: boolean |
| | | } |
| | | |
| | | export interface Workers { |
| | | export interface Worker { |
| | | workerId: string |
| | | workerName: string |
| | | phoneNum: string |
| | |
| | | export interface TasksResponse { |
| | | Tasks: Task[] |
| | | TaskCount: number |
| | | workers: Workers[] |
| | | workers: Worker[] |
| | | Prompt: Prompt |
| | | ChannelAmount: number |
| | | } |
| | | |
| | | export interface TasksGroupByChannel { |
| | | [channel: number]: TasksResponse |
| | | } |
| | |
| | | |
| | | declare module 'vue' { |
| | | export interface GlobalComponents { |
| | | DashboardLayout: typeof import('./components/DashboardLayout.vue')['default'] |
| | | ElButton: typeof import('element-plus/es')['ElButton'] |
| | | ElCollapse: typeof import('element-plus/es')['ElCollapse'] |
| | | ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] |
| | | ElIcon: typeof import('element-plus/es')['ElIcon'] |
| | | ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] |
| | | RouterLink: typeof import('vue-router')['RouterLink'] |
| | | RouterView: typeof import('vue-router')['RouterView'] |
| | | DashboardLayout: (typeof import('./components/DashboardLayout.vue'))['default'] |
| | | RouterLink: (typeof import('vue-router'))['RouterLink'] |
| | | RouterView: (typeof import('vue-router'))['RouterView'] |
| | | } |
| | | } |
| | |
| | | <slot name="leftBlock1"></slot> |
| | | </div> |
| | | <div class="double-height-block padding-4"> |
| | | <div class="card"> |
| | | <el-scrollbar always> |
| | | <div class="card scroll-card"> |
| | | <el-scrollbar always class="scroller"> |
| | | <slot name="leftBlock2"></slot> |
| | | </el-scrollbar> |
| | | </div> |
| | |
| | | <div class="header-block padding-4"> |
| | | <slot name="middleBlock1"></slot> |
| | | </div> |
| | | <div class="base-block padding-4"> |
| | | <div class="top-block padding-4"> |
| | | <div class="card"> |
| | | <slot name="middleBlock2"></slot> |
| | | </div> |
| | | </div> |
| | | <div class="block-container"> |
| | | <div class="base-block padding-4"> |
| | | <div class="bottom-block"> |
| | | <div class="bottom-block-item padding-4"> |
| | | <div class="card"> |
| | | <slot name="middleBlock3"></slot> |
| | | </div> |
| | | </div> |
| | | <div class="base-block padding-4"> |
| | | <div class="bottom-block-item padding-4"> |
| | | <div class="card"> |
| | | <slot name="middleBlock4"></slot> |
| | | </div> |
| | |
| | | $baseBlockHeight: calc((100vh - 2 * $layoutPadding - $headerBlockHeight) / 2); |
| | | // 双倍高布局块高度 |
| | | $doubleBlockHeight: calc($baseBlockHeight * 2); |
| | | // 上边固定高 |
| | | $topBlocHeight: 400px; |
| | | // 下边高度 |
| | | $bottomBlockHeight: calc(100vh - 2 * $layoutPadding - $headerBlockHeight - $topBlocHeight); |
| | | .dashboard-layout { |
| | | display: flex; |
| | | align-items: center; |
| | |
| | | .padding-4 { |
| | | padding: 6px; |
| | | } |
| | | .block-container { |
| | | |
| | | .base-block { |
| | | height: $baseBlockHeight; |
| | | flex: 1; |
| | | } |
| | | .top-block { |
| | | height: $topBlocHeight; |
| | | flex: 0; |
| | | } |
| | | .bottom-block { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | height: $bottomBlockHeight; |
| | | } |
| | | .base-block { |
| | | height: $baseBlockHeight; |
| | | flex-grow: 1; |
| | | .bottom-block-item { |
| | | height: $bottomBlockHeight; |
| | | flex: 1; |
| | | } |
| | | .header-block { |
| | | height: $headerBlockHeight; |
| | |
| | | border-radius: 6px; |
| | | padding: 10px 16px; |
| | | } |
| | | .scroll-card { |
| | | padding: 0; |
| | | } |
| | | .scroller { |
| | | padding: 10px 16px; |
| | | } |
| | | </style> |
New file |
| | |
| | | import { computed, ref } from 'vue' |
| | | import { defineStore } from 'pinia' |
| | | import type { Task, TasksGroupByChannel, TasksResponse } from '@/api/task' |
| | | import type { TaskListParams } from '@/api' |
| | | import { getTaskList } from '@/api' |
| | | |
| | | export interface ChannelMoreBtnStatus { |
| | | /** true 任务未加载完 false 所有任务已经加载完成*/ |
| | | [channel: number]: boolean |
| | | } |
| | | |
| | | export const useTasksStore = defineStore('tasks', () => { |
| | | const channels = ref<TasksGroupByChannel>({}) |
| | | |
| | | const currentType = ref<1 | 2 | 3>(1) |
| | | |
| | | /** |
| | | * 获取任务数据 |
| | | * @param type 1未完成2今天未完成3已完成 |
| | | */ |
| | | function getChannels(type: 1 | 2 | 3) { |
| | | currentType.value = type |
| | | const params: TaskListParams = { |
| | | type, |
| | | offset: 0, |
| | | limit: 3 |
| | | } |
| | | getTaskList(params) |
| | | .then((res) => { |
| | | channels.value = res.data |
| | | }) |
| | | .catch((err) => { |
| | | console.error(err) |
| | | channels.value = [] |
| | | }) |
| | | } |
| | | |
| | | function moreChannelTasksBtn(channelNumber: number) { |
| | | const taskLength = channels.value[channelNumber].Tasks?.length ?? 0 |
| | | const params: TaskListParams = { |
| | | type: currentType.value, |
| | | channel: channelNumber, |
| | | offset: taskLength, |
| | | limit: 10 |
| | | } |
| | | getTaskList(params) |
| | | .then((res) => { |
| | | const existTasks = channels.value![channelNumber].Tasks ?? [] |
| | | channels.value[channelNumber] = res.data[channelNumber] ?? {} |
| | | channels.value[channelNumber].Tasks = channels.value[channelNumber].Tasks ?? [] |
| | | channels.value[channelNumber].Tasks = [...existTasks, ...channels.value[channelNumber].Tasks] |
| | | }) |
| | | .catch((err) => { |
| | | console.error(err) |
| | | }) |
| | | } |
| | | |
| | | function foldChannelTasksBtn(channelNumber: number) { |
| | | const tasks = channels.value[channelNumber].Tasks ?? [] |
| | | channels.value[channelNumber].Tasks = tasks.slice(0, 3) |
| | | } |
| | | |
| | | const moreBtnStatus = computed(() => { |
| | | return Object.entries(channels.value).reduce((pre, currentValue) => { |
| | | const channelNumber = +currentValue[0] |
| | | const channelData = currentValue[1] as TasksResponse |
| | | pre[channelNumber] = channelData.TaskCount > (channelData.Tasks?.length ?? 0) |
| | | return pre |
| | | }, {} as ChannelMoreBtnStatus) |
| | | }) |
| | | |
| | | /** 当前高亮的任务 */ |
| | | const activeTask = ref<Task>() |
| | | function setActiveTask(task: Task) { |
| | | activeTask.value = task |
| | | } |
| | | |
| | | const requestParamsMap = ref<{ |
| | | [channel: number]: TaskListParams |
| | | }>({}) |
| | | function getParamsByChannel(channel: number) { |
| | | return ( |
| | | requestParamsMap.value[channel] ?? { |
| | | type: 1, |
| | | offset: 0, |
| | | limit: 3 |
| | | } |
| | | ) |
| | | } |
| | | |
| | | function setParamsByChannel(channel: number, params: TaskListParams) { |
| | | requestParamsMap.value[channel] = params |
| | | } |
| | | return { |
| | | channels, |
| | | getChannels, |
| | | moreBtnStatus, |
| | | activeTask, |
| | | setActiveTask, |
| | | requestParamsMap, |
| | | getParamsByChannel, |
| | | setParamsByChannel, |
| | | moreChannelTasksBtn, |
| | | foldChannelTasksBtn |
| | | } |
| | | }) |
| | |
| | | <div class="channel-collapse"> |
| | | <el-collapse v-model="activeChannel"> |
| | | <el-collapse-item |
| | | v-for="(tasks, channelNumber) in channels" |
| | | v-for="(channel, channelNumber) in channels" |
| | | :key="channelNumber" |
| | | :title="CHANNEL_NAME_MAP[channelNumber] + ' 通道'" |
| | | :name="String(channelNumber)" |
| | | > |
| | | <TaskInfo v-for="task in tasks" :key="task.Procedure.ID" :task="task"></TaskInfo> |
| | | <TaskInfo |
| | | v-for="task in channel.Tasks" |
| | | :key="task.Procedure.ID" |
| | | :active="task.Procedure.ID === tasksStore.activeTask?.Procedure.ID" |
| | | :task="task" |
| | | style="margin-bottom: 16px" |
| | | @click="tasksStore.setActiveTask(task)" |
| | | ></TaskInfo> |
| | | |
| | | <div |
| | | v-show="channel.Tasks?.length && tasksStore.moreBtnStatus?.[channelNumber]" |
| | | class="btn more" |
| | | @click="tasksStore.moreChannelTasksBtn(channelNumber)" |
| | | > |
| | | 查看更多 |
| | | <el-icon style="margin-left: 6px"><ArrowDownBold /></el-icon> |
| | | </div> |
| | | |
| | | <div |
| | | v-show="channel.Tasks?.length && !tasksStore.moreBtnStatus?.[channelNumber]" |
| | | class="btn fold" |
| | | @click="tasksStore.foldChannelTasksBtn(channelNumber)" |
| | | > |
| | | 收起 |
| | | <el-icon style="margin-left: 6px"><ArrowUpBold /></el-icon> |
| | | </div> |
| | | </el-collapse-item> |
| | | </el-collapse> |
| | | </div> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import { ref, watchEffect } from 'vue' |
| | | import type { Task } from '@/api/task' |
| | | import type { TasksGroupByChannel } from '@/api/task' |
| | | import TaskInfo from './TaskInfo.vue' |
| | | import { CHANNEL_NAME_MAP } from '@/common/constants' |
| | | |
| | | export interface Channel { |
| | | [channelNumber: number]: Task[] |
| | | } |
| | | import { useTasksStore } from '@/stores/tasks' |
| | | import { ArrowDownBold, ArrowUpBold } from '@element-plus/icons-vue' |
| | | |
| | | export interface ChannelCollapseProps { |
| | | channels: Channel |
| | | channels: TasksGroupByChannel |
| | | } |
| | | |
| | | const props = defineProps<ChannelCollapseProps>() |
| | | const activeChannel = ref<string[]>([]) |
| | | |
| | | const tasksStore = useTasksStore() |
| | | |
| | | watchEffect(() => { |
| | | const channelNumbers = Object.keys(props.channels).sort((a, b) => +a - +b) |
| | | // 通道数据变化后 |
| | | const channelNumbers = Object.keys(props?.channels ?? {}).sort((a, b) => +a - +b) |
| | | activeChannel.value = [...channelNumbers] |
| | | }) |
| | | </script> |
| | |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .btn { |
| | | width: 70%; |
| | | height: 50px; |
| | | margin: 0 auto; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 6px; |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | cursor: pointer; |
| | | background: linear-gradient(to right, rgb(29, 96, 212) 0%, rgb(47, 122, 251), rgb(29, 96, 212) 100%); |
| | | } |
| | | </style> |
File was renamed from src/views/dashboard/components/ProcessInfo.vue |
| | |
| | | <template> |
| | | <div class="process-info"> |
| | | <div class="craft-info"> |
| | | <div class="item-l-bng"> |
| | | <img src="~@/assets/images/process-model.png" /> |
| | | </div> |
| | |
| | | <div class="item-r-b">{{ process.name }}</div> |
| | | </div> |
| | | <div class="tip-r"> |
| | | <img src="~@/assets/images/process-tip.png" /> |
| | | <img src="~@/assets/images/process-tip.png" alt="" /> |
| | | </div> |
| | | <div class="tip-current">当前使用</div> |
| | | <div class="btn"> |
| | |
| | | </div> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import { computed, toRefs } from 'vue' |
| | | // 工艺信息 |
| | | import { toRefs } from 'vue' |
| | | |
| | | export interface ProcessInfoProps { |
| | | process: process |
| | | } |
| | |
| | | .font_weight { |
| | | font-weight: 600; |
| | | } |
| | | .process-info { |
| | | .craft-info { |
| | | width: calc(50% - 35px); |
| | | height: 110px; |
| | | padding: 23px 10px 10px; |
| | |
| | | </div> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import type { UserFilled } from '@element-plus/icons-vue' |
| | | import { computed, toRefs } from 'vue' |
| | | import { StarFilled, UserFilled } from '@element-plus/icons-vue' |
| | | import { toRefs } from 'vue' |
| | | import type { Worker } from '@/api/task' |
| | | |
| | | export interface PersonInfoProps { |
| | | person: person |
| | | person: Worker |
| | | } |
| | | |
| | | const props = defineProps<PersonInfoProps>() |
New file |
| | |
| | | <template> |
| | | <div class="processing-info"> |
| | | <div class="step"> |
| | | <el-steps |
| | | v-if="task?.AllProcedures" |
| | | :active="task.CurrentProcedureIndex ?? 0" |
| | | finish-status="success" |
| | | class="steps" |
| | | > |
| | | <el-step v-for="(item, index) in task.AllProcedures" :key="index" icon="" :title="item"></el-step> |
| | | </el-steps> |
| | | </div> |
| | | <div class="details"> |
| | | <div class="row"> |
| | | <div class="col">工单编号: {{ task?.Order?.workOrderId || '' }}</div> |
| | | <div class="col">订单编号: {{ task?.Order?.orderId || '' }}</div> |
| | | </div> |
| | | |
| | | <div class="row"> |
| | | <div class="col">产品名称: {{ task?.Order?.productName || '--' }}</div> |
| | | <div class="col">数量: {{ task?.Order?.amount || 0 }}{{ task?.Order?.unit }}</div> |
| | | </div> |
| | | <div class="row"> |
| | | <div class="col">交货日期: {{ task?.Order?.deliverDate || '--' }}</div> |
| | | <div class="col">工时: {{ task?.Procedure?.procedure?.workHours || '--' }}</div> |
| | | </div> |
| | | <div class="row"> |
| | | <div class="col"> |
| | | 起止时间: {{ formatDate(task?.Procedure?.startTime) }} |
| | | ~ |
| | | {{ formatDate(task?.Procedure?.endTime) }} |
| | | </div> |
| | | <div class="col">通道: {{ isNumber(task?.Channel) ? CHANNEL_NAME_MAP[task?.Channel] : '--' }}</div> |
| | | </div> |
| | | <div class="row"> |
| | | <div class="col">客户名称: {{ task?.Order?.customer || '' }}</div> |
| | | <div class="col">参数要求: {{ task?.Order?.parameter || '' }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="process"> |
| | | <div>完成进度:</div> |
| | | <div class="process-bar"> |
| | | <el-progress |
| | | define-back-color="#132f6e" |
| | | color="#00cc66" |
| | | text-color="#fff" |
| | | :text-inside="true" |
| | | :stroke-width="30" |
| | | :percentage="processingPercent" |
| | | ></el-progress> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | // 加工信息组件 |
| | | import type { Task } from '@/api/task' |
| | | import { computed, onUnmounted, toRefs, watch } from 'vue' |
| | | import { useDateFormat } from '@vueuse/core' |
| | | import { useRequest } from 'vue-hooks-plus' |
| | | import { getProductProgress } from '@/api' |
| | | import type { ProductProgressParams } from '@/api' |
| | | import { isNumber } from 'lodash-es' |
| | | import { CHANNEL_NAME_MAP } from '@/common/constants' |
| | | |
| | | const props = defineProps<{ |
| | | task?: Task |
| | | }>() |
| | | |
| | | const { task } = toRefs(props) |
| | | export interface Statistics { |
| | | totalNumber: number |
| | | finishNumber: number |
| | | } |
| | | |
| | | /** |
| | | * 计算生产进度 |
| | | * @param statistics |
| | | * @return 进度,0~100 |
| | | */ |
| | | function calculateProgress(statistics: Statistics): number { |
| | | if (!statistics) { |
| | | return 0 |
| | | } |
| | | |
| | | if (statistics.finishNumber === 0) { |
| | | return 0 |
| | | } |
| | | if (statistics.finishNumber === statistics.totalNumber) { |
| | | return 100 |
| | | } |
| | | |
| | | const result = Math.floor((statistics.finishNumber / statistics.totalNumber) * 100) |
| | | return result > 100 ? 100 : result |
| | | } |
| | | |
| | | /** |
| | | * 计算完成进度, 任务状态未生产固定为 0% 已完成固定为 100% 生产中则从plc获取 |
| | | */ |
| | | const processingPercent = computed(() => { |
| | | if (task?.value?.Procedure?.Status === 1) { |
| | | return 0 |
| | | } |
| | | |
| | | if (task?.value?.Procedure?.Status === 3) { |
| | | return 100 |
| | | } |
| | | |
| | | if (task?.value?.Procedure?.Status === 2) { |
| | | return calculateProgress(plcResponse?.value?.data as Statistics) |
| | | } |
| | | |
| | | return 0 |
| | | }) |
| | | |
| | | /** |
| | | * 如果任务状态是进行中, 则轮询 plc 取进度 |
| | | */ |
| | | const { |
| | | data: plcResponse, |
| | | run: startPLCPolling, |
| | | cancel: cancelPLCPolling |
| | | } = useRequest( |
| | | () => |
| | | getProductProgress({ |
| | | channel: task?.value?.Channel, |
| | | procedureId: task?.value?.Procedure.ID |
| | | } as ProductProgressParams), |
| | | { |
| | | manual: true, |
| | | pollingInterval: 6000, |
| | | pollingWhenHidden: false |
| | | } |
| | | ) |
| | | |
| | | /** |
| | | * 任务状态是生产中则轮询plc取目标数和完成数计算完成进度 |
| | | */ |
| | | watch( |
| | | () => task?.value, |
| | | () => { |
| | | cancelPLCPolling() |
| | | if (task?.value?.Procedure?.Status === 2) { |
| | | startPLCPolling() |
| | | } |
| | | } |
| | | ) |
| | | |
| | | onUnmounted(() => { |
| | | cancelPLCPolling() |
| | | }) |
| | | |
| | | /** |
| | | * 格式化时间戳 |
| | | * @param timestamp 后端返的10位时间戳 |
| | | */ |
| | | function formatDate(timestamp?: number) { |
| | | if (!timestamp) { |
| | | return '--' |
| | | } |
| | | const time = useDateFormat(timestamp * 1000, 'YYYY-MM-DD', { locales: 'zh-cn' }) |
| | | return time.value |
| | | } |
| | | </script> |
| | | <style scoped lang="scss"> |
| | | $text-color: #d7d7ca; |
| | | |
| | | .step { |
| | | width: 100%; |
| | | height: 66px; |
| | | overflow-x: auto; |
| | | margin-top: -5px; |
| | | padding: 0 20px; |
| | | .steps { |
| | | height: 100%; |
| | | .el-step__icon { |
| | | width: 16px; |
| | | height: 16px; |
| | | } |
| | | .el-step__title { |
| | | line-height: 25px; |
| | | font-size: 14px; |
| | | } |
| | | .el-step__title.is-process { |
| | | color: #a8abb2; |
| | | } |
| | | } |
| | | } |
| | | .details { |
| | | font-size: 18px; |
| | | padding: 10px 20px; |
| | | color: $text-color; |
| | | .row { |
| | | width: 100%; |
| | | padding: 2px 0; |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | .col { |
| | | width: 50%; |
| | | flex: 1; |
| | | } |
| | | } |
| | | .process { |
| | | font-size: 18px; |
| | | padding: 10px 20px; |
| | | color: $text-color; |
| | | display: flex; |
| | | } |
| | | .process-bar { |
| | | flex: 1; |
| | | margin-left: 20px; |
| | | } |
| | | :deep(.el-progress-bar__outer) { |
| | | border-radius: 8px; |
| | | } |
| | | :deep(.el-progress-bar__inner) { |
| | | border-radius: 8px; |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <div class="task-info" :class="{ selected }"> |
| | | <div class="task-info" :class="{ active }"> |
| | | <div |
| | | class="task-info-title" |
| | | :class="{ |
| | |
| | | |
| | | export interface TaskInfoProps { |
| | | task: Task |
| | | selected?: boolean |
| | | active?: boolean |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<TaskInfoProps>(), { |
| | | selected: false |
| | | active: false |
| | | }) |
| | | const { task, selected } = toRefs(props) |
| | | const { task, active } = toRefs(props) |
| | | |
| | | const planTimeText = computed(() => { |
| | | const format = (date: number) => { |
| | |
| | | $status-ready: #13235a; |
| | | $status-done: #13235a; |
| | | $text-color: #d7d7d7; |
| | | $active-color: #00dfdf; |
| | | .task-info { |
| | | background-color: #6b83ff; |
| | | border-radius: 4px; |
| | | overflow: initial; |
| | | cursor: pointer; |
| | | } |
| | | .task-info-title { |
| | | height: 34px; |
| | |
| | | flex: 1; |
| | | } |
| | | } |
| | | .active { |
| | | position: relative; |
| | | &:before { |
| | | content: ''; |
| | | width: 8px; |
| | | background-color: $active-color; |
| | | height: 100%; |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | border-radius: 6px 0 0 6px; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <div class="task-tabs"> |
| | | <div |
| | | v-for="tabName in list" |
| | | :key="tabName" |
| | | v-for="tab in list" |
| | | :key="tab.value" |
| | | class="task-tab-item triangle-tip" |
| | | :class="{ active: props.modelValue === tabName }" |
| | | @click="selectTab(tabName)" |
| | | :class="{ active: props.modelValue === tab.value }" |
| | | @click="selectTab(tab)" |
| | | > |
| | | {{ tabName }} |
| | | {{ tab.label }} |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import { useVModel } from '@vueuse/core' |
| | | |
| | | export interface LabelValue { |
| | | label: string |
| | | value: any |
| | | } |
| | | |
| | | const props = defineProps<{ |
| | | /** tab 列表*/ |
| | | list: string[] |
| | | list: LabelValue[] |
| | | /** 当前选中的 tab*/ |
| | | modelValue?: string |
| | | modelValue?: any |
| | | }>() |
| | | const emit = defineEmits<{ |
| | | 'update:modelValue': [tabName: string] |
| | | change: [tab: LabelValue] |
| | | }>() |
| | | const data = useVModel(props, 'modelValue', emit) |
| | | |
| | | function selectTab(tabName: string) { |
| | | data.value = tabName |
| | | function selectTab(tab: LabelValue) { |
| | | data.value = tab.value |
| | | emit('change', tab) |
| | | } |
| | | </script> |
| | | <style scoped lang="scss"> |
| | |
| | | server: { |
| | | proxy: { |
| | | '/v1/': { |
| | | target: 'http://192.168.20.4:8003', |
| | | target: 'http://192.168.20.119:8003', |
| | | ws: true, |
| | | changeOrigin: true |
| | | } |