焊接首页

This commit is contained in:
2026-06-10 14:17:54 +08:00
parent d171a5b73b
commit 787ad93dee
24 changed files with 1682 additions and 38 deletions
+9 -1
View File
@@ -3,5 +3,13 @@ import {
post post
} from '@/utils/request.js' } from '@/utils/request.js'
/**
* 登录接口
*/
export const reqLogin = (data) => post('user/postLoginOn', data) export const reqLogin = (data) => post('user/postLoginOn', data)
/**
* 请求控制权限
*/
export const reqMenuPowerList = (userId,projectId)=>get(`User/getMenuPowerList?userId=${userId}&projectId=${projectId}`)
+11
View File
@@ -0,0 +1,11 @@
import {
get,
post
} from '@/utils/request.js'
// BaseInfo/getProjectWorkArea?projectId=0792b983-4de2-44d5-adba-361b7fc99a97&
/**
* 请求单位工程
*/
export const reqProjectWorkArea = (projectId)=>get(`BaseInfo/getProjectWorkArea?projectId=${projectId}`)
+9
View File
@@ -0,0 +1,9 @@
import {
get,
post
} from '@/utils/request.js'
/**
* 获取焊接首页数据
*/
export const reqHJIndexData = (projectId)=>get(`HJGLIndex/GetJGLIndexItem?projectId=${projectId}`)
+207 -1
View File
@@ -26,6 +26,8 @@
// 首页 // 首页
.home { .home {
padding-top: 80px;
padding-bottom: 60rpx;
.u-border-bottom:after { .u-border-bottom:after {
border: none; border: none;
} }
@@ -40,7 +42,9 @@
} }
&-icons { &-icons {
height: 160rpx; position: fixed;
top: 95px;
height: 80px;
width: 100%; width: 100%;
background-color: $u-type-primary; background-color: $u-type-primary;
display: flex; display: flex;
@@ -145,4 +149,206 @@
} }
} }
} }
// 快捷应用 & 业务查询公共样式
.section-card {
margin: 20rpx;
background: #fff;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
padding: 24rpx;
.section-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
font-size: 28rpx;
font-weight: 600;
color: #333;
.title-left {
display: flex;
align-items: center;
}
.title-line {
display: inline-block;
width: 8rpx;
height: 28rpx;
background: $u-type-primary;
border-radius: 4rpx;
margin-right: 16rpx;
}
.edit-btn {
padding: 8rpx;
opacity: 0.8;
&:active {
opacity: 1;
}
}
}
.section-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24rpx 16rpx;
overflow-x: hidden;
}
.grid-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 16rpx 8rpx;
border-radius: 12rpx;
transition: background 0.2s;
min-width: 0;
&:active {
background: #f5f6f8;
}
.icon-box {
width: 100rpx;
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.item-label {
margin-top: 12rpx;
font-size: 22rpx;
color: #666;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
}
}
// 编辑快捷应用弹窗样式
.edit-quick {
padding: 32rpx 32rpx 0 32rpx;
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
.edit-title {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 28rpx;
font-weight: 600;
color: #333;
padding-bottom: 24rpx;
border-bottom: 1rpx solid #f0f0f0;
.edit-tip {
font-size: 24rpx;
color: #999;
font-weight: 400;
}
}
.edit-grid {
width: 100%;
overflow-y: scroll;
display: grid;
grid-template-columns: repeat(4, calc(25% - 20rpx));
gap: 20rpx;
padding: 24rpx 0;
min-height: 0;
box-sizing: border-box;
}
.edit-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 160rpx;
background: #f8f9fa;
border-radius: 16rpx;
position: relative;
transition: all 0.25s;
box-sizing: border-box;
&.active {
background: rgba($color: #4a7df5, $alpha: 0.08);
border: 2rpx solid $u-type-primary;
}
.icon-box {
width: 96rpx;
height: 96rpx;
display: flex;
align-items: center;
justify-content: center;
}
.edit-label {
margin-top: 14rpx;
font-size: 22rpx;
color: #666;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
.selected-mark {
position: absolute;
top: 6rpx;
right: 6rpx;
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background: $u-type-primary;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba($color: $u-type-primary, $alpha: 0.3);
}
}
.edit-btns {
display: flex;
gap: 24rpx;
padding-top: 20rpx;
padding-bottom: 10rpx;
position: absolute;
bottom: 10px;left:0;right:0;
.u-size-medium {
width: 40%;
}
.btn-cancel,
.btn-confirm {
height: 88rpx;
font-size: 30rpx;
border: none;
&::after {
border: none;
}
}
.btn-cancel {
background: #f5f6f8;
color: #666;
}
.btn-confirm {
box-shadow: 0 4rpx 16rpx rgba($color: $u-type-primary, $alpha: 0.25);
}
}
}
} }
+23 -3
View File
@@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "nbd-icon"; /* Project id 5193086 */ font-family: "nbd-icon"; /* Project id 5193086 */
src: url('//at.alicdn.com/t/c/font_5193086_zgg3m1cv22e.woff2?t=1781003671405') format('woff2'), src: url('//at.alicdn.com/t/c/font_5193086_dyc00qt3bz.woff2?t=1781056075220') format('woff2'),
url('//at.alicdn.com/t/c/font_5193086_zgg3m1cv22e.woff?t=1781003671405') format('woff'), url('//at.alicdn.com/t/c/font_5193086_dyc00qt3bz.woff?t=1781056075220') format('woff'),
url('//at.alicdn.com/t/c/font_5193086_zgg3m1cv22e.ttf?t=1781003671405') format('truetype'); url('//at.alicdn.com/t/c/font_5193086_dyc00qt3bz.ttf?t=1781056075220') format('truetype');
} }
.nbd-icon { .nbd-icon {
@@ -13,6 +13,26 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.nbd-icon-a-facheguanli:before {
content: "\e683";
}
.nbd-icon-yuzhijian:before {
content: "\e647";
}
.nbd-icon-baozhuangguanli:before {
content: "\e60e";
}
.nbd-icon-diankouguanli:before {
content: "\e60f";
}
.nbd-icon-shiyabao:before {
content: "\e657";
}
.nbd-icon-daiban:before { .nbd-icon-daiban:before {
content: "\e612"; content: "\e612";
} }
+73 -4
View File
@@ -12,9 +12,13 @@
</view> </view>
<scroll-view scroll-y="true" class="project-select-inner"> <scroll-view scroll-y="true" class="project-select-inner">
<view class="project-select-inner-wrap"> <view class="project-select-inner-wrap">
<view class="li" v-for="(item,idx) in props.list" :key="idx" <view class="li" v-for="(item,idx) in filteredList" :key="idx"
:class="selectObj[props.valueKey] == item[props.valueKey]?'active':''" @click="handerChangList(item)">{{item[props.labelKey]}} :class="selectObj && selectObj[props.valueKey] == item[props.valueKey] ? 'active' : ''"
@click="handerChangList(item)"
>
{{ item[props.labelKey] }}
</view> </view>
<view v-if="filteredList.length === 0" class="empty-tip">暂无匹配项目</view>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
@@ -24,7 +28,8 @@
<script setup> <script setup>
import { import {
ref, ref,
watch watch,
computed
} from 'vue'; } from 'vue';
const props = defineProps({ const props = defineProps({
@@ -61,6 +66,14 @@
const selectObj = ref(null) const selectObj = ref(null)
// 过滤后的列表
const filteredList = computed(() => {
if (!props.searchKey) return props.list
return props.list.filter(item =>
item[props.labelKey]?.includes(props.searchKey)
)
})
watch(() => props.modelValue, (val) => { watch(() => props.modelValue, (val) => {
visible.value = val visible.value = val
if(val){ if(val){
@@ -68,7 +81,7 @@
if (selectedItem) { if (selectedItem) {
selectObj.value = selectedItem selectObj.value = selectedItem
} else { } else {
selectObj.value = {} selectObj.value = null
} }
} }
}) })
@@ -92,3 +105,59 @@
emit('searchChange', val) emit('searchChange', val)
} }
</script> </script>
<style lang="scss" scoped>
.project-select {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
.project-select-btns {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.project-select-filter {
padding: 16rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.project-select-inner {
flex: 1;
overflow-y: auto;
.project-select-inner-wrap {
padding: 0 32rpx 32rpx;
.li {
padding: 28rpx 0;
border-bottom: 1rpx solid #f5f6f8;
font-size: 28rpx;
color: #333;
transition: all 0.2s;
&:last-child {
border-bottom: none;
}
&.active {
color: $u-type-primary;
font-weight: 500;
}
}
.empty-tip {
text-align: center;
padding: 80rpx 0;
color: #999;
font-size: 26rpx;
}
}
}
}
</style>
+325
View File
@@ -0,0 +1,325 @@
<template>
<u-popup v-model="localVisible" mode="bottom" :height="height">
<view class="nbd-select">
<!-- 标题栏 -->
<view class="nbd-select-header" v-if="title">
<text class="nbd-select-title">{{ title }}</text>
<text class="nbd-select-tip" v-if="tip">{{ tip }}</text>
</view>
<!-- 搜索栏 -->
<view class="nbd-select-filter" v-if="showSearch">
<u-search
:placeholder="searchPlaceholder"
:height="72"
v-model="searchKey"
:show-action="false"
@change="handleSearch"
@clear="handleSearch"
></u-search>
</view>
<!-- 列表内容 -->
<scroll-view scroll-y="true" class="nbd-select-inner">
<view class="nbd-select-inner-wrap">
<!-- 空状态 -->
<view class="nbd-select-empty" v-if="filteredList.length === 0">
<u-empty mode="list" text="暂无数据"></u-empty>
</view>
<!-- 列表项 -->
<view
v-for="(item, idx) in filteredList"
:key="idx"
class="nbd-select-item"
:class="{ active: isSelected(item) }"
@click="handleItemClick(item)"
>
<text class="nbd-select-item-text">{{ item[labelKey] }}</text>
<view v-if="multiple" class="nbd-select-checkbox">
<u-icon
:name="isSelected(item) ? 'checkbox-mark' : 'square'"
:size="36"
:color="isSelected(item) ? $u.type.primary : '#dcdee0'"
></u-icon>
</view>
</view>
</view>
</scroll-view>
<!-- 底部按钮 -->
<view class="nbd-select-footer">
<u-button class="nbd-select-btn-cancel" @click="handleCancel">
{{ cancelText }}
</u-button>
<u-button class="nbd-select-btn-confirm" type="primary" @click="handleConfirm">
{{ confirmText }}
</u-button>
</view>
</view>
</u-popup>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const props = defineProps({
// 弹窗显示状态
modelValue: {
type: Boolean,
default: false
},
// 弹窗高度
height: {
type: String,
default: '70%'
},
// 标题
title: {
type: String,
default: ''
},
// 标题提示
tip: {
type: String,
default: ''
},
// 数据列表
list: {
type: Array,
default: () => []
},
// 选中项(支持数组,多选时使用)
model: {
type: [Object, Array, String, Number],
default: null
},
// 是否多选
multiple: {
type: Boolean,
default: false
},
// 显示搜索框
showSearch: {
type: Boolean,
default: false
},
// 搜索占位符
searchPlaceholder: {
type: String,
default: '请输入关键词搜索'
},
// 字段名配置
labelKey: {
type: String,
default: 'label'
},
valueKey: {
type: String,
default: 'value'
},
// 自定义搜索字段(默认搜索 labelKey)
searchKeyField: {
type: String,
default: ''
},
// 按钮文字
cancelText: {
type: String,
default: '取消'
},
confirmText: {
type: String,
default: '确定'
}
})
const emit = defineEmits([
'update:modelValue',
'update:model',
'cancel',
'confirm',
'searchChange',
'change'
])
const localVisible = ref(false)
const searchKey = ref('')
// 当前选中的项(多选时为数组)
const selectedItems = ref(props.multiple ? (props.model || []) : (props.model || null))
// 监听外部 model 变化
watch(() => props.model, (newVal) => {
selectedItems.value = props.multiple ? (newVal || []) : newVal
}, { deep: true })
// 弹窗状态同步
watch(() => props.modelValue, (val) => {
localVisible.value = val
if (val) {
// 打开时重置搜索
searchKey.value = ''
}
})
watch(localVisible, (val) => {
emit('update:modelValue', val)
})
// 过滤后的列表
const filteredList = computed(() => {
if (!searchKey.value) return props.list
const keyword = searchKey.value.toLowerCase()
const searchField = props.searchKeyField || props.labelKey
return props.list.filter(item => {
const value = item[searchField]
if (value === null || value === undefined) return false
return String(value).toLowerCase().includes(keyword)
})
})
// 判断是否选中
const isSelected = (item) => {
if (props.multiple) {
return selectedItems.value.some(
selected => selected[props.valueKey] === item[props.valueKey]
)
}
if (selectedItems.value && typeof selectedItems.value === 'object') {
return selectedItems.value[props.valueKey] === item[props.valueKey]
}
return selectedItems.value === item[props.valueKey]
}
// 点击列表项
const handleItemClick = (item) => {
if (props.multiple) {
const index = selectedItems.value.findIndex(
selected => selected[props.valueKey] === item[props.valueKey]
)
if (index > -1) {
selectedItems.value.splice(index, 1)
} else {
selectedItems.value.push(item)
}
} else {
selectedItems.value = item
}
emit('change', selectedItems.value)
}
// 搜索
const handleSearch = (val) => {
emit('searchChange', val)
}
// 取消
const handleCancel = () => {
emit('cancel')
localVisible.value = false
}
// 确认
const handleConfirm = () => {
emit('confirm', selectedItems.value)
emit('update:model', selectedItems.value)
localVisible.value = false
}
</script>
<style lang="scss" scoped>
.nbd-select {
display: flex;
flex-direction: column;
height: 100%;
background: #ffffff;
border-radius: 24rpx 24rpx 0 0;
overflow: hidden;
&-header {
display: flex;
flex-direction: column;
padding: 24rpx 32rpx 20rpx;
border-bottom: 1rpx solid #f0f0f0;
.nbd-select-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.nbd-select-tip {
font-size: 24rpx;
color: #999;
}
}
&-filter {
padding: 16rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
&-inner {
flex: 1;
overflow-y: auto;
padding: 0 32rpx;
&-wrap {
padding: 16rpx 0;
}
}
&-empty {
padding: 80rpx 0;
}
&-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 0;
border-bottom: 1rpx solid #f5f6f8;
transition: background 0.2s;
&:last-child {
border-bottom: none;
}
&.active .nbd-select-item-text {
color: $u-type-primary;
font-weight: 500;
}
.nbd-select-item-text {
flex: 1;
font-size: 28rpx;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nbd-select-checkbox {
margin-left: 16rpx;
}
}
&-footer {
display: flex;
gap: 24rpx;
padding: 20rpx 32rpx 40rpx;
.nbd-select-btn-cancel,
.nbd-select-btn-confirm {
flex: 1;
height: 80rpx;
font-size: 28rpx;
border-radius: 40rpx;
}
}
}
</style>
+39
View File
@@ -20,8 +20,47 @@
"navigationBarTitleText": "首页", "navigationBarTitleText": "首页",
"navigationStyle": "custom" "navigationStyle": "custom"
} }
},
{
"path": "pages/mine/index",
"style": {
"navigationBarTitleText": "我的"
}
} }
], ],
"subPackages": [
{
"root": "pipe",
"pages": [
{
"path": "diankou/list",
"style": {
"navigationBarTitleText": "点口管理"
}
}
]
}
],
"tabBar": {
"color": "#cbcbcb",
"backgroundColor": "#2979ff",
"selectedColor": "#ffffff",
"borderStyle": "black",
"list": [{
"text": "首页",
"pagePath": "pages/index/index",
"iconPath": "/static/tabs/home.png",
"selectedIconPath": "/static/tabs/home-active.png"
},
{
"text": "个人中心",
"pagePath": "pages/mine/index",
"iconPath": "static/tabs/mine.png",
"selectedIconPath": "static/tabs/mine-active.png"
}
],
"position": "bottom"
},
"globalStyle": { "globalStyle": {
"navigationBarTextStyle": "white", "navigationBarTextStyle": "white",
"navigationBarTitleText": "施工管理", "navigationBarTitleText": "施工管理",
+130 -15
View File
@@ -18,48 +18,112 @@
</view> </view>
</view> </view>
<view class="home-hj"> <view class="home-hj">
<view class="hj-title">项目管道实时预制率</view>
<view class="hj-content"> <view class="hj-content">
<view class="hj-row"> <view class="hj-row">
<view class="hj-cell"> <view class="hj-cell">
<text class="hj-label">工厂焊接一次合格率</text> <text class="hj-label">工厂焊接一次合格率</text>
<text class="hj-value">99.5%</text> <text class="hj-value">{{hjIndexData?.GCFirstPassRate}}</text>
</view> </view>
<view class="hj-cell"> <view class="hj-cell">
<text class="hj-label">现场焊接一次合格率</text> <text class="hj-label">现场焊接一次合格率</text>
<text class="hj-value">0%</text> <text class="hj-value">{{hjIndexData?.XCFirstPassRate}}</text>
</view> </view>
</view> </view>
<view class="hj-row"> <view class="hj-row">
<view class="hj-cell"> <view class="hj-cell">
<text class="hj-label">工厂预制进度</text> <text class="hj-label">工厂预制进度</text>
<text class="hj-value hj-sub">49229.500/286639.000 DIN</text> <text class="hj-value hj-sub">{{hjIndexData?.GCProgress}}</text>
</view> </view>
<view class="hj-cell"> <view class="hj-cell">
<text class="hj-label">现场焊接进度</text> <text class="hj-label">现场焊接进度</text>
<text class="hj-value hj-sub">5906.500/84951.000 DIN</text> <text class="hj-value hj-sub">{{hjIndexData?.XCProgress}}</text>
</view> </view>
</view> </view>
<view class="hj-row"> <view class="hj-row">
<view class="hj-cell"> <view class="hj-cell">
<text class="hj-label">工厂焊工功效</text> <text class="hj-label">工厂焊工功效</text>
<text class="hj-value">252 DIN/</text> <text class="hj-value">{{hjIndexData?.GCWelderEfficacy}}</text>
</view> </view>
<view class="hj-cell"> <view class="hj-cell">
<text class="hj-label">现场焊接功效</text> <text class="hj-label">现场焊接功效</text>
<text class="hj-value">95 DIN/</text> <text class="hj-value">{{hjIndexData?.XCWelderEfficacy}}</text>
</view> </view>
</view> </view>
</view> </view>
<view class="hj-footer"> <view class="hj-footer">
<text class="hj-footer-label">项目管道实时预制率 (包含工厂和现场的预制口)</text> <text class="hj-footer-label">项目管道实时预制率 (包含工厂和现场的预制口)</text>
<text class="hj-footer-value">13%</text> <text class="hj-footer-value">{{hjIndexData?.PipePrefabricationRate}}</text>
</view>
</view>
<!-- 快捷应用 -->
<view class="section-card home-quick">
<view class="section-title">
<view class="title-left">
<text class="title-line"></text>
<text>快捷应用</text>
</view>
<u-icon class="edit-btn" name="edit-pen" :size="32" color="#999" @click="openEditQuick" />
</view>
<view class="section-grid">
<view class="grid-item" v-for="(item, idx) in quickApps" :key="idx" @click="handleSkip(item)">
<view class="icon-box">
<u-icon :name="item.icon" :size="56" :custom-prefix='item.prefix'
:color="pastelColors[idx % 6]" />
</view>
<text class="item-label">{{ item.name }}</text>
</view>
</view> </view>
</view> </view>
<!-- 业务查询 -->
<view class="section-card home-query" v-if="queryApps&&queryApps.length>0">
<view class="section-title">
<view class="title-left">
<text class="title-line"></text>
<text>业务查询</text>
</view>
</view>
<view class="section-grid">
<view class="grid-item" v-for="(item, idx) in queryApps" :key="idx" @click="handleSkip(item)">
<view class="icon-box">
<u-icon :name="item.icon" :custom-prefix='item.prefix' :size="56" :color="item.color" />
</view>
<text class="item-label">{{ item.name }}</text>
</view>
</view>
</view>
<!-- 编辑快捷应用弹窗 -->
<u-popup v-model="showEditQuick" mode="bottom" height="70%">
<view class="edit-quick">
<view class="edit-title">
<text>选择快捷应用</text>
</view>
<view class="edit-grid">
<view v-for="(item, idx) in editCache" :key="idx" class="edit-item"
:class="{ active: item.selected }" @click="toggleSelect(item)">
<view class="icon-box">
<u-icon :name="item.icon" :size="48" :custom-prefix='item.prefix'
:color="item.selected ? pastelColors[idx % 6] : '#ccc'" />
</view>
<text class="edit-label">{{ item.name }}</text>
<view v-if="item.selected" class="selected-mark">
<u-icon name="checkmark" :size="24" color="#fff" />
</view>
</view>
</view>
<view class="edit-btns">
<u-button size="medium" class="btn-cancel" @click="showEditQuick = false">取消</u-button>
<u-button size="medium" class="btn-confirm" type="primary" @click="handleConfirmEdit">确定</u-button>
</view>
</view>
</u-popup>
<nbd-select-project v-model="showProject" :select-id="currentProject?.ProjectId" :list="projectList" <nbd-select-project v-model="showProject" :select-id="currentProject?.ProjectId" :list="projectList"
label-key="ProjectName" value-key="ProjectId" @confirm="handleConfirmProject" /> label-key="ProjectName" value-key="ProjectId" :search-key="projectNameKeyword"
@search-change="handleSearchChange" @confirm="handleConfirmProject" />
</view> </view>
</template> </template>
@@ -73,13 +137,18 @@
ref ref
} from 'vue' } from 'vue'
import { import {
useUserStore useUserStore,
useMenuStore,
useLargeStore
} from '@/store'; } from '@/store';
import { import {
storeToRefs storeToRefs
} from 'pinia'; } from 'pinia';
const userStore = useUserStore() const userStore = useUserStore()
const menuStore = useMenuStore()
const largeStore = useLargeStore()
const { const {
setProjectList, setProjectList,
setCurrentProject setCurrentProject
@@ -89,12 +158,52 @@
projectList, projectList,
currentProject currentProject
} = storeToRefs(userStore) } = storeToRefs(userStore)
const {
quickApps,
queryApps,
editCache
} = storeToRefs(menuStore)
const {
hjIndexData
} = storeToRefs(largeStore)
const {
pastelColors
} = menuStore
const navbarBg = reactive({ const navbarBg = reactive({
backgroundColor: "var(--u-type-primary)" backgroundColor: "var(--u-type-primary)"
}) })
const projectNameKeyword = ref('')
const showProject = ref(false) const showProject = ref(false)
const showEditQuick = ref(false)
const currentProjectName = computed(()=>currentProject.value?.ProjectName||'') const currentProjectName = computed(() => currentProject.value?.ProjectName || '')
const handleSkip= (item) => {
uni.navigateTo({
url: item.path
})
}
// 打开编辑弹窗
const openEditQuick = () => {
menuStore.openEdit()
showEditQuick.value = true
}
// 切换选择状态
const toggleSelect = (item) => {
menuStore.toggleSelect(item)
}
// 确认编辑
const handleConfirmEdit = () => {
menuStore.confirmEdit()
showEditQuick.value = false
uni.showToast({
title: '已更新快捷应用',
icon: 'success'
})
}
const handleShowProjectSelect = () => { const handleShowProjectSelect = () => {
reqProjectByUserId(userInfo.value.PersonId).then(res => { reqProjectByUserId(userInfo.value.PersonId).then(res => {
@@ -105,23 +214,29 @@
/** /**
* 项目列表点击确认 * 项目列表点击确认
*/ */
const handleConfirmProject = (item)=>{ const handleConfirmProject = (item) => {
setCurrentProject(item) setCurrentProject(item)
showProject.value = false showProject.value = false
} }
/**
* 项目按名称搜索
*/
const handleSearchChange = (keyword) => {
projectNameKeyword.value = keyword
}
/** /**
* 扫一扫 * 扫一扫
*/ */
const handleScan = ()=>{ const handleScan = () => {
uni.scanCode({ uni.scanCode({
onlyFromCamera: true, onlyFromCamera: true,
success (res) { success(res) {
console.log('条码类型:' + res.scanType); console.log('条码类型:' + res.scanType);
console.log('条码内容:' + res.result); console.log('条码内容:' + res.result);
}, },
fail(err) { fail(err) {
console.log("扫码失败了====>",err) console.log("扫码失败了====>", err)
} }
}); });
} }
+277
View File
@@ -0,0 +1,277 @@
<template>
<view class="mine">
<!-- 用户信息卡片 -->
<view class="user-card">
<view class="user-info">
<u-avatar :size="120" icon="account" bgColor="#e0e7ff"></u-avatar>
<view class="user-detail">
<text class="user-name">{{ userInfo?.PersonName || '未登录' }}</text>
<text class="user-account">账号{{ userInfo?.Account || '-' }}</text>
<text class="user-company">{{ userInfo?.CompanyName || '暂无公司信息' }}</text>
<text class="user-phone">{{ userInfo?.Telephone || '暂无手机号' }}</text>
</view>
</view>
<u-button v-if="!token" size="large" shape="circle" @click="goLogin">
立即登录
</u-button>
</view>
<!-- 项目信息 -->
<view class="project-card" v-if="currentProject?.ProjectId">
<view class="card-title">
<u-icon name="home" :size="36" color="#2979ff"></u-icon>
<text>当前项目</text>
</view>
<view class="project-info">
<text class="project-name">{{ currentProject.ProjectName }}</text>
</view>
</view>
<!-- 功能菜单 -->
<view class="menu-list">
<view class="menu-item" @click="handleMenuClick('profile')">
<view class="menu-left">
<u-icon name="setting" :size="40" color="#2979ff"></u-icon>
<text>个人设置</text>
</view>
<u-icon name="arrow-right" :size="28" color="#ccc"></u-icon>
</view>
<view class="menu-item" @click="handleMenuClick('password')">
<view class="menu-left">
<u-icon name="lock" :size="40" color="#2979ff"></u-icon>
<text>修改密码</text>
</view>
<u-icon name="arrow-right" :size="28" color="#ccc"></u-icon>
</view>
<view class="menu-item" @click="handleMenuClick('about')">
<view class="menu-left">
<u-icon name="info-circle" :size="40" color="#2979ff"></u-icon>
<text>关于我们</text>
</view>
<u-icon name="arrow-right" :size="28" color="#ccc"></u-icon>
</view>
</view>
<!-- 退出登录 -->
<view class="logout-btn" v-if="token">
<u-button text="退出登录" @click="handleLogout"></u-button>
</view>
</view>
</template>
<script setup>
import {
computed
} from 'vue'
import {
useUserStore
} from '@/store'
import {
storeToRefs
} from 'pinia'
const userStore = useUserStore()
const {
logout
} = userStore
const {
userInfo,
token,
currentProject
} = storeToRefs(userStore)
// 跳转登录
const goLogin = () => {
uni.reLaunch({
url: '/pages/login/index'
})
}
// 切换项目
const handleSwitchProject = () => {
// TODO: 打开项目选择弹窗
uni.showToast({
title: '项目选择功能开发中',
icon: 'none'
})
}
// 菜单点击
const handleMenuClick = (type) => {
const menuMap = {
profile: '个人设置',
password: '修改密码',
about: '关于我们'
}
uni.showToast({
title: `${menuMap[type]}功能开发中`,
icon: 'none'
})
}
// 退出登录
const handleLogout = () => {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
logout()
uni.reLaunch({
url: '/pages/login/index'
})
}
}
})
}
</script>
<style lang="scss" scoped>
.mine {
min-height: 100vh;
background: #f5f6f8;
padding: 32rpx;
}
// 用户信息卡片
.user-card {
background: linear-gradient(135deg, #2979ff 0%, #6ba7ff 100%);
border-radius: 24rpx;
padding: 40rpx 32rpx;
margin-bottom: 24rpx;
color: #ffffff;
.user-info {
display: flex;
align-items: flex-start;
margin-bottom: 32rpx;
.user-detail {
margin-left: 24rpx;
display: flex;
flex-direction: column;
flex: 1;
.user-name {
font-size: 36rpx;
font-weight: 600;
margin-bottom: 8rpx;
}
.user-account,
.user-company,
.user-phone {
font-size: 24rpx;
opacity: 0.85;
margin-bottom: 4rpx;
}
.user-account {
color: rgba(255, 255, 255, 0.9);
}
.user-company {
color: rgba(255, 255, 255, 0.9);
}
.user-phone {
color: rgba(255, 255, 255, 0.8);
}
}
}
.u-button {
background: rgba(255, 255, 255, 0.2);
border: none;
color: #ffffff;
&::after {
border: none;
}
}
}
// 项目信息卡片
.project-card {
background: #ffffff;
border-radius: 24rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
.card-title {
display: flex;
align-items: center;
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 24rpx;
.u-icon {
margin-right: 12rpx;
}
}
.project-info {
display: flex;
align-items: center;
justify-content: space-between;
.project-name {
font-size: 28rpx;
color: #666;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
// 功能菜单
.menu-list {
background: #ffffff;
border-radius: 24rpx;
padding: 0 32rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
.menu-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 0;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.menu-left {
display: flex;
align-items: center;
.u-icon {
margin-right: 16rpx;
}
text {
font-size: 28rpx;
color: #333;
}
}
}
}
// 退出登录
.logout-btn {
margin-top: 48rpx;
.u-button {
height: 88rpx;
font-size: 30rpx;
border-radius: 44rpx;
}
}
</style>
+371
View File
@@ -0,0 +1,371 @@
<template>
<view class="page">
<!-- 头部过滤区域 -->
<view class="filter-header">
<view class="filter-row">
<!-- 下拉过滤 1 -->
<u-dropdown v-model="filter1Value" :options="filter1Options" @change="handleFilter1Change"></u-dropdown>
<!-- 下拉过滤 2 -->
<u-dropdown v-model="filter2Value" :options="filter2Options" @change="handleFilter2Change"></u-dropdown>
<!-- 刷新按钮 -->
<view class="refresh-btn" @click="handleRefresh">
<u-icon name="reload" :size="36" :class="{ rotating: loading }"></u-icon>
</view>
</view>
</view>
<!-- 列表内容 -->
<scroll-view
class="list-scroll"
scroll-y
:scroll-into-view="scrollIntoView"
@scrolltolower="loadMore"
refresher-enabled
:refresher-triggered="refreshing"
@refresh="handleRefresh"
>
<!-- 列表项 -->
<view class="list-wrap">
<view
v-for="(item, index) in listData"
:key="item.id || index"
class="list-item"
@click="handleItemClick(item)"
>
<view class="item-header">
<text class="item-title">{{ item.name || '未命名' }}</text>
<text class="item-status" :class="item.status">{{ item.statusText || '' }}</text>
</view>
<view class="item-content">
<text class="item-desc">{{ item.description || '' }}</text>
</view>
<view class="item-footer">
<text class="item-time">{{ item.createTime || '' }}</text>
<text class="item-author">{{ item.creator || '' }}</text>
</view>
</view>
<!-- 空状态 -->
<u-empty
v-if="!loading && listData.length === 0"
mode="list"
text="暂无数据"
></u-empty>
<!-- 加载提示 -->
<view v-if="loading && listData.length > 0" class="loading-more">
<u-loading mode="circle" size="32"></u-loading>
<text>加载中...</text>
</view>
<!-- 没有更多 -->
<view v-if="!loading && !hasMore && listData.length > 0" class="no-more">
<text> 没有更多了 </text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
// ===== 过滤条件 =====
const filter1Value = ref('')
const filter2Value = ref('')
// 过滤选项 1
const filter1Options = ref([
{ label: '全部类型', value: '' },
{ label: '类型 A', value: 'type_a' },
{ label: '类型 B', value: 'type_b' },
{ label: '类型 C', value: 'type_c' }
])
// 过滤选项 2
const filter2Options = ref([
{ label: '全部状态', value: '' },
{ label: '进行中', value: 'pending' },
{ label: '已完成', value: 'completed' },
{ label: '已取消', value: 'cancelled' }
])
// ===== 列表数据 =====
const listData = ref([])
const loading = ref(false)
const refreshing = ref(false)
const hasMore = ref(true)
const currentPage = ref(1)
const pageSize = 20
const scrollIntoView = ref('')
// ===== 筛选条件变更 =====
const filterParams = reactive({
filter1: '',
filter2: ''
})
// 过滤 1 变化
const handleFilter1Change = (value) => {
filterParams.filter1 = value
resetAndLoad()
}
// 过滤 2 变化
const handleFilter2Change = (value) => {
filterParams.filter2 = value
resetAndLoad()
}
// ===== 数据加载 =====
// 加载数据
const fetchData = async (isRefresh = false) => {
if (loading.value || (!isRefresh && !hasMore.value)) return
loading.value = true
if (isRefresh) {
refreshing.value = true
}
try {
// TODO: 替换为实际 API 调用
// const res = await api.getList({
// page: currentPage.value,
// pageSize,
// ...filterParams
// })
// 模拟数据
const mockData = await mockApiFetch({
page: currentPage.value,
pageSize,
...filterParams
})
if (isRefresh) {
listData.value = mockData.list
} else {
listData.value = [...listData.value, ...mockData.list]
}
hasMore.value = mockData.hasMore
currentPage.value++
} catch (error) {
console.error('加载数据失败:', error)
uni.showToast({
title: '加载失败,请重试',
icon: 'none'
})
} finally {
loading.value = false
refreshing.value = false
}
}
// 重置并重新加载
const resetAndLoad = () => {
currentPage.value = 1
hasMore.value = true
listData.value = []
fetchData(true)
}
// 下拉刷新
const handleRefresh = () => {
resetAndLoad()
}
// 上拉加载更多
const loadMore = () => {
if (!loading.value && hasMore.value) {
fetchData(false)
}
}
// 列表项点击
const handleItemClick = (item) => {
console.log('点击列表项:', item)
// TODO: 跳转详情
// uni.navigateTo({ url: `/pages/xxx/detail?id=${item.id}` })
}
// ===== 模拟 API(实际使用时删除)=====
const mockApiFetch = async (params) => {
return new Promise((resolve) => {
setTimeout(() => {
const start = (params.page - 1) * params.pageSize
const end = start + params.pageSize
const hasMore = end < 100 // 模拟总共 100 条数据
const list = Array.from({ length: hasMore ? params.pageSize : (100 - start) }, (_, i) => ({
id: start + i + 1,
name: `项目 ${start + i + 1}`,
description: '这是一个测试项目的描述信息,可能包含更多内容',
status: ['pending', 'completed', 'cancelled'][Math.floor(Math.random() * 3)],
statusText: ['进行中', '已完成', '已取消'][Math.floor(Math.random() * 3)],
createTime: '2024-01-01 12:00:00',
creator: `用户${Math.floor(Math.random() * 10) + 1}`
}))
resolve({
list,
hasMore
})
}, 500)
})
}
// ===== 生命周期 =====
onMounted(() => {
fetchData(true)
})
</script>
<style lang="scss" scoped>
.page {
display: flex;
flex-direction: column;
height: 100vh;
background: #f5f6f8;
}
// 头部过滤区域
.filter-header {
background: #ffffff;
padding: 20rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
.filter-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.refresh-btn {
margin-left: auto;
padding: 8rpx;
display: flex;
align-items: center;
justify-content: center;
.rotating {
animation: rotate 1s linear infinite;
}
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
// 列表滚动区域
.list-scroll {
flex: 1;
overflow: hidden;
}
.list-wrap {
padding: 24rpx;
}
// 列表项
.list-item {
background: #ffffff;
border-radius: 16rpx;
padding: 28rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
&:last-child {
margin-bottom: 0;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.item-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.item-status {
font-size: 22rpx;
padding: 6rpx 16rpx;
border-radius: 20rpx;
background: #f0f0f0;
color: #666;
&.pending {
background: rgba(255, 167, 38, 0.1);
color: #ffa726;
}
&.completed {
background: rgba(76, 175, 80, 0.1);
color: #4caf50;
}
&.cancelled {
background: rgba(244, 67, 54, 0.1);
color: #f44336;
}
}
}
.item-content {
margin-bottom: 16rpx;
.item-desc {
font-size: 26rpx;
color: #666;
line-height: 1.6;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
.item-footer {
display: flex;
justify-content: space-between;
.item-time,
.item-author {
font-size: 22rpx;
color: #999;
}
}
}
// 加载中
.loading-more {
display: flex;
flex-direction: column;
align-items: center;
padding: 32rpx 0;
color: #999;
font-size: 24rpx;
}
// 没有更多
.no-more {
text-align: center;
padding: 32rpx 0;
color: #ccc;
font-size: 24rpx;
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

+2
View File
@@ -8,4 +8,6 @@ export function setupStore(app) {
} }
export * from './modules/user' export * from './modules/user'
export * from './modules/menu'
export * from './modules/large'
export default pinia export default pinia
+25
View File
@@ -0,0 +1,25 @@
import {
defineStore
} from 'pinia'
import {
ref,
computed
} from 'vue'
import {reqHJIndexData} from '@/api/hj.js'
export const useLargeStore = defineStore('large', () => {
const hjIndexData = ref(null)
/**
* 获取焊接首页数据
*/
const fetchHJIndex = (projectId)=>{
reqHJIndexData(projectId).then(res=>{
hjIndexData.value = res.data
})
}
return {
hjIndexData,
fetchHJIndex
}
})
+102
View File
@@ -0,0 +1,102 @@
import {
defineStore
} from 'pinia'
import {
ref,
computed
} from 'vue'
import {pastelColors,defaultApps} from '@/utils/constant.js'
import {reqMenuPowerList} from '@/api/auth.js'
export const useMenuStore = defineStore('menu', () => {
// 应用列表(默认配置 + 持久化)
const appsList = ref([...defaultApps])
// 编辑时的临时缓存
const editCache = ref([])
// 菜单的权限列表(后端返回的有权限的菜单 code 数组)
const menuProwerList = ref([])
// 快捷应用列表(已选择的)
const quickApps = computed(() => appsList.value.filter(app => app.selected&&hasMenuPermission(app.menuId)))
// 业务查询列表(未选择的)
const queryApps = computed(() => appsList.value.filter(app => !app.selected&&hasMenuPermission(app.menuId)))
/**
* 打开编辑弹窗(备份当前状态)
*/
const openEdit = () => {
editCache.value = appsList.value.filter(app => hasMenuPermission(app.menuId))
}
/**
* 切换应用选择状态
*/
const toggleSelect = (item) => {
item.selected = !item.selected
}
/**
* 确认编辑(保存更改)
*/
const confirmEdit = () => {
appsList.value = editCache.value.map(app => ({ ...app }))
}
/**
* 请求菜单权限列表
*/
const fetchMenuPowerList = async (userId, projectId) => {
if (!userId || !projectId) return
try {
const res = await reqMenuPowerList(userId, projectId)
menuProwerList.value = res.data || []
} catch (err) {
console.error('获取菜单权限失败:', err)
menuProwerList.value = []
}
}
/**
* 判断菜单是否有权限打开
* @param {string} menuCode - 菜单编码/标识
* @returns {boolean} 是否有权限
*/
const hasMenuPermission = (menuCode) => {
if (!menuCode) return true
return menuProwerList.value.includes(menuCode)
}
/**
* 根据权限过滤快捷应用
*/
const quickAppsWithPermission = computed(() => {
if (menuProwerList.value.length === 0) return quickApps.value
return quickApps.value.filter(app => {
return hasMenuPermission(app.permissionCode)
})
})
return {
appsList,
editCache,
quickApps,
quickAppsWithPermission,
queryApps,
menuProwerList,
pastelColors,
openEdit,
toggleSelect,
confirmEdit,
fetchMenuPowerList,
hasMenuPermission
}
}, {
persist: {
enabled: true,
strategies: [{
key: 'menu',
paths: [],
}]
},
})
+24 -8
View File
@@ -2,12 +2,13 @@ import {
defineStore defineStore
} from 'pinia' } from 'pinia'
import { import {
ref ref,
watch
} from 'vue' } from 'vue'
import { import {
reqLogin reqLogin
} from '@/api/auth.js' } from '@/api/auth.js'
import { useMenuStore, useLargeStore } from '@/store'
export const useUserStore = defineStore('user', () => { export const useUserStore = defineStore('user', () => {
const token = ref("") const token = ref("")
const userInfo = ref(null) const userInfo = ref(null)
@@ -20,6 +21,20 @@ export const useUserStore = defineStore('user', () => {
const projectList = ref([]) const projectList = ref([])
const currentProject = ref(null) const currentProject = ref(null)
// 监听 currentProject 变化,获取菜单权限
watch(() => currentProject.value?.ProjectId, (newId) => {
if (newId) {
// 项目变化 请求菜单权限
const menuStore = useMenuStore()
const userId = userInfo.value?.PersonId
menuStore.fetchMenuPowerList(userId, newId)
// 项目变化 请求焊接首页数据
const largeStore = useLargeStore()
largeStore.fetchHJIndex(newId)
}
}, { immediate: true })
/** /**
* 登出 * 登出
@@ -27,11 +42,11 @@ export const useUserStore = defineStore('user', () => {
const logout = () => { const logout = () => {
token.value = "" token.value = ""
userInfo.value = null userInfo.value = null
loginForm.value = { // loginForm.value = {
account: '', // account: '',
password: '', // password: '',
Telephone: '', // Telephone: '',
} // }
currentProject.value = null currentProject.value = null
} }
@@ -79,7 +94,8 @@ export const useUserStore = defineStore('user', () => {
projectList, projectList,
currentProject, currentProject,
setProjectList, setProjectList,
setCurrentProject setCurrentProject,
logout
} }
}, { }, {
persist: { persist: {
+49
View File
@@ -0,0 +1,49 @@
// 淡雅配色方案(马卡龙色系)
export const pastelColors = [
'#7eb6ff', // 淡蓝
'#ffb787', // 淡橙
'#8fd6a8', // 淡绿
'#b99bf8', // 淡紫
'#ff8fa3', // 淡粉
'#80deea', // 青色
'#ffd56b', // 淡黄
]
// 默认应用配置
export const defaultApps = [{
name: '点口管理',
icon: 'diankouguanli',
menuId: '3ACE25CE-C5CE-4CEC-AD27-0D5CF1DF2F01',
path: "/pipe/diankou/list",
prefix: 'nbd-icon',
selected: true
},
{
name: '试压管理',
icon: 'shiyabao',
menuId: '55976B16-2C33-406E-B514-2FE42D031071',
prefix: 'nbd-icon',
selected: true
},
{
name: '包装管理',
icon: 'baozhuangguanli',
menuId: "25DED954-10C9-47CC-99F2-C44FDE9E0A81",
prefix: 'nbd-icon',
selected: true
},
{
name: '发货管理',
icon: 'a-facheguanli',
menuId: "EEC0D060-C15E-4D25-B015-C2B91F735DAC",
prefix: 'nbd-icon',
selected: true
},
{
name: '预制组件管理',
icon: 'yuzhijian',
menuId: "8255554C-0A92-4C7B-BF19-779AF0220A8C",
prefix: 'nbd-icon',
selected: true
}
]