chore: initialize mini program frontend
This commit is contained in:
@@ -0,0 +1,634 @@
|
||||
const api = require('../../utils/api')
|
||||
const locationGuard = require('../../utils/locationGuard')
|
||||
|
||||
const avatarKey = phone => `jh_wrs_avatar_${phone}`
|
||||
|
||||
Page({
|
||||
data: {
|
||||
activeTab: 'home',
|
||||
currentUser: null,
|
||||
currentRoleName: '',
|
||||
currentRolesText: '',
|
||||
canSwitchRole: false,
|
||||
avatarPath: '',
|
||||
avatarText: '',
|
||||
roleChoices: [],
|
||||
selectionToken: '',
|
||||
summary: {},
|
||||
showOverview: false,
|
||||
showWorkerTrend: false,
|
||||
workerTrend: {
|
||||
total: 0,
|
||||
max: 0,
|
||||
points: [],
|
||||
segments: [],
|
||||
},
|
||||
notices: [],
|
||||
noticeList: [],
|
||||
actions: [],
|
||||
currentAttendancePointName: '',
|
||||
locationStatusText: '当前位置:未进入考勤点',
|
||||
locationEntered: false,
|
||||
locationInitialized: false,
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
temporaryWorkerModalVisible: false,
|
||||
temporaryWorkerPrompt: '',
|
||||
temporaryWorkerName: '',
|
||||
},
|
||||
onShow() {
|
||||
this.load({ refreshLocation: !this.data.locationInitialized }).catch(error => {
|
||||
wx.showToast({ title: error.message || '加载失败', icon: 'none' })
|
||||
})
|
||||
},
|
||||
async load(options = {}) {
|
||||
let currentUser = api.getCurrentUser()
|
||||
let summary = {}
|
||||
let showOverview = false
|
||||
let showWorkerTrend = false
|
||||
let workerTrend = { total: 0, max: 0, points: [], segments: [] }
|
||||
let notices = []
|
||||
let locationView = {
|
||||
currentAttendancePointName: this.data.currentAttendancePointName,
|
||||
locationStatusText: this.data.locationStatusText,
|
||||
locationEntered: this.data.locationEntered,
|
||||
}
|
||||
let locationInitialized = this.data.locationInitialized
|
||||
if (currentUser) {
|
||||
try {
|
||||
currentUser = await api.refreshCurrentUser() || currentUser
|
||||
showOverview = currentUser.role !== 'worker'
|
||||
showWorkerTrend = currentUser.role === 'worker'
|
||||
if (showOverview) {
|
||||
summary = await api.getHomeSummary(currentUser)
|
||||
}
|
||||
if (showWorkerTrend) {
|
||||
workerTrend = await this.loadWorkerTrend()
|
||||
}
|
||||
} catch (error) {
|
||||
wx.showToast({ title: error.message, icon: 'none' })
|
||||
currentUser = api.getCurrentUser()
|
||||
showOverview = !!currentUser && currentUser.role !== 'worker'
|
||||
showWorkerTrend = !!currentUser && currentUser.role === 'worker'
|
||||
}
|
||||
if (currentUser) {
|
||||
notices = await this.loadNotices()
|
||||
if (options.refreshLocation || !locationInitialized) {
|
||||
locationView = await this.refreshLocationStatus()
|
||||
locationInitialized = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
locationInitialized = false
|
||||
locationView = {
|
||||
currentAttendancePointName: '',
|
||||
locationStatusText: '当前位置:未进入考勤点',
|
||||
locationEntered: false,
|
||||
}
|
||||
}
|
||||
const actions = this.buildActions(currentUser)
|
||||
const overviewItems = this.buildOverviewItems(currentUser, summary)
|
||||
const userView = this.buildUserView(currentUser)
|
||||
this.setData({
|
||||
currentUser,
|
||||
summary,
|
||||
overviewItems,
|
||||
showOverview,
|
||||
showWorkerTrend,
|
||||
workerTrend,
|
||||
notices,
|
||||
noticeList: notices.slice(0, 3),
|
||||
actions,
|
||||
...locationView,
|
||||
locationInitialized,
|
||||
...userView,
|
||||
})
|
||||
},
|
||||
async onPullDownRefresh() {
|
||||
this.setData({ refreshing: true })
|
||||
try {
|
||||
await this.load({ refreshLocation: true })
|
||||
} finally {
|
||||
this.setData({ refreshing: false })
|
||||
if (wx.stopPullDownRefresh) {
|
||||
wx.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
},
|
||||
async loadNotices() {
|
||||
try {
|
||||
const result = await api.listNotices({ page: 1, pageSize: 6 })
|
||||
return result.rows || []
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
},
|
||||
async refreshLocationStatus() {
|
||||
try {
|
||||
const result = await locationGuard.getCurrentAttendancePoint()
|
||||
if (result && result.factory) {
|
||||
return {
|
||||
currentAttendancePointName: result.factory.name,
|
||||
locationStatusText: `当前位置:已进入 ${result.factory.name} 考勤点`,
|
||||
locationEntered: true,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
currentAttendancePointName: '',
|
||||
locationStatusText: '当前位置:未进入考勤点',
|
||||
locationEntered: false,
|
||||
}
|
||||
}
|
||||
return {
|
||||
currentAttendancePointName: '',
|
||||
locationStatusText: '当前位置:未进入考勤点',
|
||||
locationEntered: false,
|
||||
}
|
||||
},
|
||||
buildUserView(user) {
|
||||
if (!user) {
|
||||
return {
|
||||
currentRoleName: '',
|
||||
currentRolesText: '',
|
||||
canSwitchRole: false,
|
||||
avatarPath: '',
|
||||
avatarText: '',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
currentRoleName: user.roleName || api.roleNames[user.role] || user.role,
|
||||
currentRolesText: (user.roleNames || []).join('、') || user.roleName || '',
|
||||
canSwitchRole: !!(user.rolesDisplay && user.rolesDisplay.length > 1),
|
||||
avatarPath: wx.getStorageSync(avatarKey(user.phone)) || '',
|
||||
avatarText: this.buildDefaultAvatarText(user.name),
|
||||
}
|
||||
},
|
||||
buildOverviewItems(user, summary = {}) {
|
||||
if (!user || user.role === 'worker') {
|
||||
return []
|
||||
}
|
||||
const today = this.formatDate(new Date())
|
||||
return [
|
||||
{
|
||||
key: 'products',
|
||||
label: '产品',
|
||||
value: summary.productCount || 0,
|
||||
url: '/pages/manageProducts/manageProducts?readonly=1',
|
||||
},
|
||||
{
|
||||
key: 'people',
|
||||
label: '人员',
|
||||
value: summary.peopleCount || 0,
|
||||
url: '/pages/managePeople/managePeople?readonly=1',
|
||||
},
|
||||
...(user.role === 'admin' ? [{
|
||||
key: 'pending',
|
||||
label: '待审核',
|
||||
value: summary.pendingCount || 0,
|
||||
alert: (summary.pendingCount || 0) > 0,
|
||||
url: '/pages/records/records?mode=pending&status=pending',
|
||||
}] : []),
|
||||
...(user.role === 'admin' ? [{
|
||||
key: 'approved',
|
||||
label: '今日通过',
|
||||
value: summary.approvedToday || 0,
|
||||
url: `/pages/records/records?mode=audit&status=approved&startDate=${today}&endDate=${today}`,
|
||||
}] : []),
|
||||
]
|
||||
},
|
||||
buildDefaultAvatarText(name) {
|
||||
const text = String(name || '').trim()
|
||||
if (!text) {
|
||||
return '嘉'
|
||||
}
|
||||
const givenName = text.length > 1 ? text.slice(1) : text
|
||||
return givenName.slice(0, 2)
|
||||
},
|
||||
switchTab(e) {
|
||||
const activeTab = e.currentTarget.dataset.tab
|
||||
this.setData({ activeTab })
|
||||
},
|
||||
buildRecentDates(days = 7) {
|
||||
const dates = []
|
||||
const today = new Date()
|
||||
for (let index = days - 1; index >= 0; index -= 1) {
|
||||
const date = new Date(today)
|
||||
date.setDate(today.getDate() - index)
|
||||
const value = this.formatDate(date)
|
||||
dates.push({
|
||||
value,
|
||||
label: value.slice(5),
|
||||
})
|
||||
}
|
||||
return dates
|
||||
},
|
||||
formatDate(date) {
|
||||
const year = date.getFullYear()
|
||||
const monthValue = date.getMonth() + 1
|
||||
const dayValue = date.getDate()
|
||||
const month = monthValue < 10 ? `0${monthValue}` : `${monthValue}`
|
||||
const day = dayValue < 10 ? `0${dayValue}` : `${dayValue}`
|
||||
return `${year}-${month}-${day}`
|
||||
},
|
||||
async loadWorkerTrend() {
|
||||
const dates = this.buildRecentDates(7)
|
||||
const result = await api.listReports({
|
||||
startDate: dates[0].value,
|
||||
endDate: dates[dates.length - 1].value,
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
})
|
||||
const totals = dates.reduce((map, item) => {
|
||||
map[item.value] = 0
|
||||
return map
|
||||
}, {})
|
||||
;(result.rows || []).forEach(report => {
|
||||
if (totals[report.reportDate] === undefined) {
|
||||
return
|
||||
}
|
||||
totals[report.reportDate] += Number(report.metrics && report.metrics.totalGood || 0)
|
||||
})
|
||||
const points = dates.map(item => ({
|
||||
date: item.value,
|
||||
label: item.label,
|
||||
value: totals[item.value] || 0,
|
||||
}))
|
||||
const total = points.reduce((sum, item) => sum + item.value, 0)
|
||||
const max = points.reduce((largest, item) => Math.max(largest, item.value), 0)
|
||||
return this.buildTrendView(total, max, points)
|
||||
},
|
||||
buildTrendView(total, max, points) {
|
||||
const maxValue = Math.max(1, max || 0)
|
||||
const lastIndex = Math.max(1, points.length - 1)
|
||||
const chartInset = 8
|
||||
const chartSpan = 100 - chartInset * 2
|
||||
const plottedPoints = points.map((item, index) => {
|
||||
const left = chartInset + (index * chartSpan / lastIndex)
|
||||
const top = 82 - (Number(item.value || 0) / maxValue) * 68
|
||||
return {
|
||||
...item,
|
||||
left: left.toFixed(2),
|
||||
top: top.toFixed(2),
|
||||
edgeClass: index === 0 ? 'first' : (index === points.length - 1 ? 'last' : ''),
|
||||
valueText: String(item.value || 0),
|
||||
}
|
||||
})
|
||||
const segments = []
|
||||
for (let index = 0; index < plottedPoints.length - 1; index += 1) {
|
||||
const start = plottedPoints[index]
|
||||
const end = plottedPoints[index + 1]
|
||||
const dx = Number(end.left) - Number(start.left)
|
||||
const dy = Number(end.top) - Number(start.top)
|
||||
const dxRpx = dx * 5.6
|
||||
const dyRpx = dy * 2.2
|
||||
const width = Math.sqrt(dxRpx * dxRpx + dyRpx * dyRpx) / 5.6
|
||||
const angle = Math.atan2(dyRpx, dxRpx) * 180 / Math.PI
|
||||
segments.push({
|
||||
left: start.left,
|
||||
top: start.top,
|
||||
width: width.toFixed(2),
|
||||
angle: angle.toFixed(2),
|
||||
})
|
||||
}
|
||||
return { total, max, points: plottedPoints, segments }
|
||||
},
|
||||
buildActions(user) {
|
||||
if (!user) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (user.role === 'worker') {
|
||||
return [
|
||||
{ key: 'records', icon: '记', title: '我的报工记录', desc: '按日期查看提交和审核状态', url: '/pages/records/records' },
|
||||
]
|
||||
}
|
||||
|
||||
if (user.role === 'admin') {
|
||||
return [
|
||||
{ key: 'products', icon: '产', title: '产品清单', desc: '导入、添加、修改产品', url: '/pages/manageProducts/manageProducts' },
|
||||
{ key: 'people', icon: '人', title: '人员清单', desc: '维护员工、管理员和经理', url: '/pages/managePeople/managePeople' },
|
||||
{ key: 'review', icon: '审', title: '报工审核', desc: '处理待审核报工', url: '/pages/review/review' },
|
||||
{ key: 'audit', icon: '核', title: '审核记录', desc: '查看已审核报工', url: '/pages/records/records?mode=audit' },
|
||||
{ key: 'monitor', icon: '监', title: '智能监控', desc: '查看模具占用和反馈', url: '/pages/smartMonitor/smartMonitor' },
|
||||
{ key: 'usageStats', icon: '统', title: '设备模具使用统计', desc: '按时间统计设备和模具使用', url: '/pages/usageStats/usageStats' },
|
||||
{ key: 'moldQr', icon: '码', title: '模具二维码', desc: '生成模具扫码入口', url: '/pages/deviceQr/deviceQr' },
|
||||
{ key: 'equipment', icon: '设', title: '设备管理', desc: '维护设备号和备注', url: '/pages/manageEquipment/manageEquipment' },
|
||||
{ key: 'notices', icon: '通', title: '通知配置', desc: '维护首页通知公告', url: '/pages/manageNotices/manageNotices' },
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{ key: 'products', icon: '产', title: '产品清单', desc: '导入或添加新产品', url: '/pages/manageProducts/manageProducts' },
|
||||
{ key: 'people', icon: '人', title: '人员清单', desc: '维护员工、管理员和经理', url: '/pages/managePeople/managePeople' },
|
||||
{ key: 'schedule', icon: '勤', title: '考勤设置', desc: '维护作息时间和考勤范围', url: '/pages/workSchedule/workSchedule' },
|
||||
{ key: 'ledger', icon: '账', title: '对账小账本', desc: '按月核对最后工序成品数量', url: '/pages/reconciliationLedger/reconciliationLedger' },
|
||||
{ key: 'dashboard', icon: '看', title: '报工看板', desc: '按日期查看工人日报', url: '/pages/dashboard/dashboard' },
|
||||
{ key: 'usageStats', icon: '统', title: '设备模具使用统计', desc: '按时间统计设备和模具使用', url: '/pages/usageStats/usageStats' },
|
||||
{ key: 'notices', icon: '通', title: '通知配置', desc: '维护首页通知公告', url: '/pages/manageNotices/manageNotices' },
|
||||
]
|
||||
},
|
||||
decodeRepeated(value) {
|
||||
let text = String(value || '').trim()
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
try {
|
||||
const decoded = decodeURIComponent(text)
|
||||
if (decoded === text) {
|
||||
break
|
||||
}
|
||||
text = decoded
|
||||
} catch (error) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return text
|
||||
},
|
||||
extractMoldName(scanResult) {
|
||||
const raw = this.decodeRepeated(scanResult)
|
||||
if (!raw) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const patterns = [
|
||||
/(?:^|[?&])moldName=([^&]+)/i,
|
||||
/(?:^|[?&])mold_name=([^&]+)/i,
|
||||
/(?:^|[?&])deviceNo=([^&]+)/i,
|
||||
/(?:^|[?&])device_no=([^&]+)/i,
|
||||
/(?:^|[?&])scene=([^&]+)/i,
|
||||
/moldName=([^&]+)/i,
|
||||
/deviceNo=([^&]+)/i,
|
||||
]
|
||||
for (let index = 0; index < patterns.length; index += 1) {
|
||||
const matched = raw.match(patterns[index])
|
||||
if (matched && matched[1]) {
|
||||
const value = this.decodeRepeated(matched[1])
|
||||
if (value.includes('moldName=') || value.includes('deviceNo=')) {
|
||||
return this.extractMoldName(value)
|
||||
}
|
||||
return String(value || '').trim()
|
||||
}
|
||||
}
|
||||
|
||||
return raw
|
||||
},
|
||||
async scanDevice() {
|
||||
wx.showLoading({ title: '定位中' })
|
||||
try {
|
||||
await locationGuard.ensureFactoryLocation()
|
||||
} catch (error) {
|
||||
wx.hideLoading()
|
||||
wx.showModal({
|
||||
title: '无法扫码报工',
|
||||
content: error.message || '当前位置不在允许范围内',
|
||||
showCancel: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
wx.hideLoading()
|
||||
wx.scanCode({
|
||||
onlyFromCamera: false,
|
||||
success: async res => {
|
||||
const candidates = [res.path, res.result, res.rawData]
|
||||
const moldName = candidates.reduce((found, item) => (
|
||||
found || this.extractMoldName(item)
|
||||
), '')
|
||||
if (!moldName) {
|
||||
wx.showToast({ title: '未识别模具名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const paramName = moldName.startsWith('mold=') ? 'scene' : 'moldName'
|
||||
if (paramName === 'scene') {
|
||||
try {
|
||||
wx.showLoading({ title: '校验中' })
|
||||
const resolved = await api.resolveMoldScene(moldName)
|
||||
await locationGuard.ensureFactoryLocation(resolved.attendancePointName)
|
||||
} catch (error) {
|
||||
wx.showModal({
|
||||
title: '无法扫码报工',
|
||||
content: error.message || '当前位置不在该考勤点范围内',
|
||||
showCancel: false,
|
||||
})
|
||||
return
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
}
|
||||
wx.navigateTo({
|
||||
url: `/pages/clock/clock?${paramName}=${encodeURIComponent(moldName)}`,
|
||||
})
|
||||
},
|
||||
fail: error => {
|
||||
if (error.errMsg && error.errMsg.includes('cancel')) {
|
||||
return
|
||||
}
|
||||
wx.showToast({ title: '扫码失败', icon: 'none' })
|
||||
},
|
||||
})
|
||||
},
|
||||
async wechatLogin(e) {
|
||||
const phoneCode = e.detail && e.detail.code
|
||||
if (!phoneCode) {
|
||||
wx.showToast({ title: '未获得手机号授权', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '登录中' })
|
||||
try {
|
||||
const result = await api.loginWithPhoneCode(phoneCode)
|
||||
if (result.needsTemporaryWorker) {
|
||||
wx.hideLoading()
|
||||
await this.confirmTemporaryWorker(result)
|
||||
return
|
||||
}
|
||||
if (result.needsSelection) {
|
||||
this.setData({
|
||||
roleChoices: result.matchedPeople,
|
||||
selectionToken: result.selectionToken,
|
||||
})
|
||||
return
|
||||
}
|
||||
await this.load({ refreshLocation: true })
|
||||
} catch (error) {
|
||||
wx.showToast({ title: error.message, icon: 'none' })
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
},
|
||||
confirmTemporaryWorker(result) {
|
||||
return new Promise(resolve => {
|
||||
this.temporaryWorkerResult = result
|
||||
this.temporaryWorkerResolve = resolve
|
||||
this.setData({
|
||||
temporaryWorkerModalVisible: true,
|
||||
temporaryWorkerPrompt: `该手机号${result.temporaryPhone || ''}不在人员清单中。临时工账号有效期只有24小时,请填写姓名后创建。`,
|
||||
temporaryWorkerName: '',
|
||||
})
|
||||
})
|
||||
},
|
||||
onTemporaryWorkerNameInput(e) {
|
||||
this.setData({ temporaryWorkerName: e.detail.value })
|
||||
},
|
||||
closeTemporaryWorkerModal() {
|
||||
this.setData({
|
||||
temporaryWorkerModalVisible: false,
|
||||
temporaryWorkerPrompt: '',
|
||||
temporaryWorkerName: '',
|
||||
})
|
||||
this.temporaryWorkerResult = null
|
||||
if (this.temporaryWorkerResolve) {
|
||||
this.temporaryWorkerResolve()
|
||||
this.temporaryWorkerResolve = null
|
||||
}
|
||||
},
|
||||
async submitTemporaryWorker() {
|
||||
const result = this.temporaryWorkerResult
|
||||
const name = String(this.data.temporaryWorkerName || '').trim()
|
||||
if (!name) {
|
||||
wx.showToast({ title: '请输入姓名', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!result || !result.temporaryToken) {
|
||||
wx.showToast({ title: '临时工注册信息已失效,请重新登录', icon: 'none' })
|
||||
this.closeTemporaryWorkerModal()
|
||||
return
|
||||
}
|
||||
this.setData({ temporaryWorkerModalVisible: false })
|
||||
wx.showLoading({ title: '定位中' })
|
||||
try {
|
||||
const attendancePoint = await locationGuard.ensureTemporaryWorkerLocation()
|
||||
wx.showLoading({ title: '创建中' })
|
||||
await api.createTemporaryWorker(result.temporaryToken, name, attendancePoint)
|
||||
await this.load({ refreshLocation: true })
|
||||
wx.showToast({ title: '临时工已生效', icon: 'success' })
|
||||
} catch (error) {
|
||||
wx.showToast({ title: error.message, icon: 'none' })
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
this.closeTemporaryWorkerModal()
|
||||
}
|
||||
},
|
||||
logout() {
|
||||
api.logout()
|
||||
this.setData({ locationInitialized: false })
|
||||
this.load()
|
||||
},
|
||||
chooseAvatar() {
|
||||
const user = this.data.currentUser
|
||||
if (!user) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const save = tempFilePath => {
|
||||
const saveToStorage = path => {
|
||||
wx.setStorageSync(avatarKey(user.phone), path)
|
||||
this.setData({ avatarPath: path })
|
||||
}
|
||||
if (wx.saveFile) {
|
||||
wx.saveFile({
|
||||
tempFilePath,
|
||||
success: res => saveToStorage(res.savedFilePath),
|
||||
fail: () => saveToStorage(tempFilePath),
|
||||
})
|
||||
return
|
||||
}
|
||||
saveToStorage(tempFilePath)
|
||||
}
|
||||
|
||||
if (wx.chooseMedia) {
|
||||
wx.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ['image'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: res => {
|
||||
const file = res.tempFiles && res.tempFiles[0]
|
||||
if (file && file.tempFilePath) {
|
||||
save(file.tempFilePath)
|
||||
}
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
wx.chooseImage({
|
||||
count: 1,
|
||||
sourceType: ['album', 'camera'],
|
||||
success: res => {
|
||||
const path = res.tempFilePaths && res.tempFilePaths[0]
|
||||
if (path) {
|
||||
save(path)
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
switchCurrentRole() {
|
||||
const user = this.data.currentUser
|
||||
const choices = (user && user.rolesDisplay ? user.rolesDisplay : [])
|
||||
.filter(item => item.role !== user.role)
|
||||
if (!choices.length) {
|
||||
wx.showToast({ title: '没有可切换的角色', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
wx.showActionSheet({
|
||||
itemList: choices.map(item => item.roleName),
|
||||
success: async res => {
|
||||
const target = choices[res.tapIndex]
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '切换中' })
|
||||
try {
|
||||
await api.switchRole(target.role)
|
||||
await this.load({ refreshLocation: true })
|
||||
wx.showToast({ title: '已切换角色', icon: 'success' })
|
||||
} catch (error) {
|
||||
wx.showToast({ title: error.message, icon: 'none' })
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
async chooseRole(e) {
|
||||
const role = e.currentTarget.dataset.role
|
||||
wx.showLoading({ title: '登录中' })
|
||||
try {
|
||||
await api.selectRole(this.data.selectionToken, role)
|
||||
this.setData({ roleChoices: [], selectionToken: '' })
|
||||
await this.load({ refreshLocation: true })
|
||||
} catch (error) {
|
||||
wx.showToast({ title: error.message, icon: 'none' })
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
},
|
||||
openAction(e) {
|
||||
wx.navigateTo({
|
||||
url: e.currentTarget.dataset.url,
|
||||
})
|
||||
},
|
||||
openOverview(e) {
|
||||
const url = e.currentTarget.dataset.url
|
||||
const disabledText = e.currentTarget.dataset.disabledText
|
||||
if (!url) {
|
||||
wx.showToast({ title: disabledText || '当前角色无权限', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.navigateTo({ url })
|
||||
},
|
||||
noop() {},
|
||||
resetDemo() {
|
||||
wx.showModal({
|
||||
title: '退出登录',
|
||||
content: '退出当前账号后需要重新进行手机号快捷登录。',
|
||||
success: res => {
|
||||
if (res.confirm) {
|
||||
api.logout()
|
||||
this.setData({ roleChoices: [], selectionToken: '', locationInitialized: false })
|
||||
this.load()
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
<view class="index-shell">
|
||||
<scroll-view
|
||||
class="page index-page safe-bottom"
|
||||
scroll-y
|
||||
type="list"
|
||||
refresher-enabled
|
||||
refresher-triggered="{{refreshing}}"
|
||||
bindrefresherrefresh="onPullDownRefresh"
|
||||
>
|
||||
<view class="header">
|
||||
<view class="header-row">
|
||||
<view class="brand-block">
|
||||
<text class="title metallic-title">嘉恒智能报工</text>
|
||||
<text class="subtitle">宁波嘉恒智能科技有限公司</text>
|
||||
<view wx:if="{{currentUser}}" class="location-status">
|
||||
<text>当前位置:</text>
|
||||
<block wx:if="{{locationEntered}}">
|
||||
<text>已进入 </text>
|
||||
<text class="location-point-name">{{currentAttendancePointName}}</text>
|
||||
<text> 考勤点</text>
|
||||
</block>
|
||||
<text wx:else class="location-not-entered">未进入考勤点</text>
|
||||
</view>
|
||||
<text wx:if="{{currentUser}}" class="location-refresh-tip">下拉刷新位置</text>
|
||||
</view>
|
||||
<view wx:if="{{currentUser}}" class="header-user" bindtap="switchCurrentRole">
|
||||
<view class="avatar small-avatar">
|
||||
<image wx:if="{{avatarPath}}" class="avatar-image" src="{{avatarPath}}" mode="aspectFill"></image>
|
||||
<text wx:else class="avatar-text">{{avatarText}}</text>
|
||||
</view>
|
||||
<view class="header-user-text">
|
||||
<text class="header-name">{{currentUser.name}}</text>
|
||||
<view class="role-line">
|
||||
<text>{{currentRoleName}}</text>
|
||||
<text wx:if="{{canSwitchRole}}" class="role-arrow">▾</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tab-pane" hidden="{{activeTab !== 'home'}}">
|
||||
<button class="scan-btn" bindtap="scanDevice">扫一扫</button>
|
||||
|
||||
<view wx:if="{{currentUser}}" class="notice-card">
|
||||
<view class="notice-hero">
|
||||
<view class="notice-badge">通知</view>
|
||||
<swiper wx:if="{{notices.length}}" class="notice-swiper" autoplay circular vertical interval="3200" duration="420">
|
||||
<swiper-item wx:for="{{notices}}" wx:key="id">
|
||||
<view class="notice-slide">
|
||||
<text class="notice-title">{{item.title}}</text>
|
||||
<text class="notice-content">{{item.content}}</text>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view wx:else class="notice-empty">
|
||||
<text class="notice-title">暂无通知</text>
|
||||
<text class="notice-content">今天暂无新的通知。</text>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{noticeList.length}}" class="notice-list">
|
||||
<view wx:for="{{noticeList}}" wx:key="id" class="notice-list-row">
|
||||
<text class="notice-dot"></text>
|
||||
<text class="notice-list-title">{{item.title}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!currentUser}}" class="card">
|
||||
<view class="button-row">
|
||||
<button class="btn primary full" open-type="getPhoneNumber" bindgetphonenumber="wechatLogin">手机号快捷登录</button>
|
||||
</view>
|
||||
<view wx:if="{{roleChoices.length}}" class="role-list">
|
||||
<text class="label">请选择角色</text>
|
||||
<button
|
||||
wx:for="{{roleChoices}}"
|
||||
wx:key="role"
|
||||
class="role-btn"
|
||||
data-role="{{item.role}}"
|
||||
bindtap="chooseRole"
|
||||
>
|
||||
{{item.name}} · {{item.roleName}}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<block wx:if="{{showOverview}}">
|
||||
<view class="section-title">概览</view>
|
||||
<view class="grid">
|
||||
<view
|
||||
wx:for="{{overviewItems}}"
|
||||
wx:key="key"
|
||||
class="stat tappable-stat stat-{{item.key}} {{item.alert ? 'stat-alert' : ''}}"
|
||||
data-url="{{item.url}}"
|
||||
data-disabled-text="{{item.disabledText}}"
|
||||
bindtap="openOverview"
|
||||
>
|
||||
<text class="label">{{item.label}}</text>
|
||||
<text class="stat-number">{{item.value || 0}}</text>
|
||||
<text class="stat-link">查看清单</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block wx:if="{{showWorkerTrend}}">
|
||||
<view class="section-title">最近7天成品数量</view>
|
||||
<view class="card trend-card">
|
||||
<view class="row trend-head">
|
||||
<view>
|
||||
<text class="label">7天合计</text>
|
||||
<text class="value">{{workerTrend.total || 0}}</text>
|
||||
</view>
|
||||
<view class="text-right">
|
||||
<text class="label">单日最高</text>
|
||||
<text class="value">{{workerTrend.max || 0}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="trend-plot">
|
||||
<view class="trend-axis trend-axis-top">{{workerTrend.max || 0}}</view>
|
||||
<view class="trend-axis trend-axis-bottom">0</view>
|
||||
<view class="trend-plot-body">
|
||||
<view class="trend-grid-line top"></view>
|
||||
<view class="trend-grid-line middle"></view>
|
||||
<view class="trend-grid-line bottom"></view>
|
||||
<view
|
||||
wx:for="{{workerTrend.segments}}"
|
||||
wx:key="index"
|
||||
class="trend-segment"
|
||||
style="left: {{item.left}}%; top: {{item.top}}%; width: {{item.width}}%; transform: rotate({{item.angle}}deg);"
|
||||
></view>
|
||||
<view
|
||||
wx:for="{{workerTrend.points}}"
|
||||
wx:key="date"
|
||||
class="trend-point-wrap {{item.edgeClass}}"
|
||||
style="left: {{item.left}}%; top: {{item.top}}%;"
|
||||
>
|
||||
<text wx:if="{{item.value}}" class="trend-point-value">{{item.valueText}}</text>
|
||||
<text class="trend-point"></text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="trend-label-row">
|
||||
<text
|
||||
wx:for="{{workerTrend.points}}"
|
||||
wx:key="date"
|
||||
class="trend-date-label {{item.edgeClass}}"
|
||||
style="left: {{item.left}}%;"
|
||||
>{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<view class="tab-pane" hidden="{{activeTab !== 'workbench'}}">
|
||||
<view class="section-title">工作台</view>
|
||||
<view wx:if="{{!actions.length}}" class="list-empty">请选择账号</view>
|
||||
<view wx:else class="workbench-panel">
|
||||
<view class="app-grid">
|
||||
<view
|
||||
wx:for="{{actions}}"
|
||||
wx:key="key"
|
||||
class="app-item"
|
||||
data-url="{{item.url}}"
|
||||
data-action="{{item.action}}"
|
||||
bindtap="openAction"
|
||||
>
|
||||
<view class="app-icon app-icon-{{item.key}}">{{item.icon}}</view>
|
||||
<text class="app-title">{{item.title}}</text>
|
||||
<text class="app-desc">{{item.desc}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tab-pane" hidden="{{activeTab !== 'mine'}}">
|
||||
<view class="section-title">我的</view>
|
||||
|
||||
<view wx:if="{{!currentUser}}" class="card">
|
||||
<view class="button-row">
|
||||
<button class="btn primary full" open-type="getPhoneNumber" bindgetphonenumber="wechatLogin">手机号快捷登录</button>
|
||||
</view>
|
||||
<view wx:if="{{roleChoices.length}}" class="role-list">
|
||||
<text class="label">请选择角色</text>
|
||||
<button
|
||||
wx:for="{{roleChoices}}"
|
||||
wx:key="role"
|
||||
class="role-btn"
|
||||
data-role="{{item.role}}"
|
||||
bindtap="chooseRole"
|
||||
>
|
||||
{{item.name}} · {{item.roleName}}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:else class="card profile-card">
|
||||
<view class="profile-head">
|
||||
<view class="avatar large-avatar">
|
||||
<image wx:if="{{avatarPath}}" class="avatar-image" src="{{avatarPath}}" mode="aspectFill"></image>
|
||||
<text wx:else class="avatar-text large-avatar-text">{{avatarText}}</text>
|
||||
</view>
|
||||
<view class="profile-main">
|
||||
<text class="profile-name">{{currentUser.name}}</text>
|
||||
<text class="subtitle">{{currentUser.phone}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="divider"></view>
|
||||
<view class="info-row" bindtap="switchCurrentRole">
|
||||
<text class="label inline-label">当前角色</text>
|
||||
<view class="info-value">
|
||||
<text>{{currentRoleName}}</text>
|
||||
<text wx:if="{{canSwitchRole}}" class="role-arrow">▾</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label inline-label">全部角色</text>
|
||||
<text class="value">{{currentRolesText}}</text>
|
||||
</view>
|
||||
<view wx:if="{{currentUser.role !== 'manager'}}" class="info-row">
|
||||
<text class="label inline-label">考勤点</text>
|
||||
<text class="value">{{currentUser.attendancePointsText || '-'}}</text>
|
||||
</view>
|
||||
<view class="button-row">
|
||||
<button class="btn secondary full" bindtap="chooseAvatar">更换头像</button>
|
||||
<button class="btn danger full" bindtap="resetDemo">退出登录</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="bottom-bar">
|
||||
<view class="bottom-item {{activeTab === 'home' ? 'active' : ''}}" data-tab="home" bindtap="switchTab">
|
||||
<text class="bottom-icon">⌂</text>
|
||||
<text>首页</text>
|
||||
</view>
|
||||
<view class="bottom-item {{activeTab === 'workbench' ? 'active' : ''}}" data-tab="workbench" bindtap="switchTab">
|
||||
<text class="bottom-icon">▦</text>
|
||||
<text>工作台</text>
|
||||
</view>
|
||||
<view class="bottom-item {{activeTab === 'mine' ? 'active' : ''}}" data-tab="mine" bindtap="switchTab">
|
||||
<text class="bottom-icon">○</text>
|
||||
<text>我的</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{temporaryWorkerModalVisible}}" class="temp-worker-mask" catchtap="closeTemporaryWorkerModal">
|
||||
<view class="temp-worker-panel" catchtap="noop">
|
||||
<text class="temp-worker-title">创建临时工</text>
|
||||
<text class="temp-worker-tip">{{temporaryWorkerPrompt}}</text>
|
||||
<view class="temp-worker-field">
|
||||
<text class="temp-worker-label">姓名</text>
|
||||
<input
|
||||
class="input temp-worker-input"
|
||||
value="{{temporaryWorkerName}}"
|
||||
placeholder="请输入姓名"
|
||||
bindinput="onTemporaryWorkerNameInput"
|
||||
confirm-type="done"
|
||||
cursor-spacing="24"
|
||||
/>
|
||||
</view>
|
||||
<view class="button-row temp-worker-actions">
|
||||
<button class="btn secondary full" bindtap="closeTemporaryWorkerModal">取消</button>
|
||||
<button class="btn primary full" bindtap="submitTemporaryWorker">确认创建</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,806 @@
|
||||
.index-shell {
|
||||
min-height: 100vh;
|
||||
background: #f2f5f9;
|
||||
}
|
||||
|
||||
.index-page {
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + 150rpx);
|
||||
}
|
||||
|
||||
.header-row {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 18rpx;
|
||||
padding-top: 8rpx;
|
||||
}
|
||||
|
||||
.index-page .header::after {
|
||||
left: 18rpx;
|
||||
right: auto;
|
||||
top: 128rpx;
|
||||
width: 150rpx;
|
||||
height: 46rpx;
|
||||
opacity: 0.06;
|
||||
transform: rotate(14deg);
|
||||
}
|
||||
|
||||
.brand-block {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.location-status {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
color: #1f2937;
|
||||
font-size: 23rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.location-refresh-tip {
|
||||
display: block;
|
||||
margin-top: 2rpx;
|
||||
color: #7a8798;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.location-not-entered {
|
||||
color: #c5221f;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.location-point-name {
|
||||
color: #087443;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.metallic-title {
|
||||
display: inline-block;
|
||||
color: #4b5563;
|
||||
font-weight: 900;
|
||||
background:
|
||||
linear-gradient(90deg, #38414d 0%, #dfe6ec 7%, #7f8b97 14%, #f8fbfd 22%, #59636f 31%, #eef3f6 41%, #3f4853 52%, #c4cdd6 64%, #ffffff 73%, #626c78 84%, #2f3741 100%),
|
||||
linear-gradient(180deg, #f9fbfc 0%, #a7b0bb 18%, #ffffff 34%, #5b6470 54%, #d3dae2 74%, #3b4450 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
-webkit-text-stroke: 1rpx rgba(47, 55, 65, 0.42);
|
||||
text-shadow:
|
||||
0 1rpx 0 rgba(255, 255, 255, 0.92),
|
||||
0 3rpx 0 rgba(87, 96, 108, 0.78),
|
||||
0 6rpx 0 rgba(38, 45, 54, 0.62),
|
||||
0 12rpx 16rpx rgba(15, 23, 42, 0.28),
|
||||
0 20rpx 28rpx rgba(15, 23, 42, 0.16);
|
||||
}
|
||||
|
||||
.header-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
flex: 0 0 auto;
|
||||
max-width: 300rpx;
|
||||
padding: 10rpx 12rpx;
|
||||
border: 1rpx solid #e1e7f0;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
box-shadow:
|
||||
0 8rpx 0 rgba(205, 216, 229, 0.54),
|
||||
0 14rpx 22rpx rgba(22, 34, 50, 0.08),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.header-user-text {
|
||||
min-width: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.header-name {
|
||||
display: block;
|
||||
color: #1f2933;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.role-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4rpx;
|
||||
margin-top: 6rpx;
|
||||
color: #667085;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.role-arrow {
|
||||
color: #1463ff;
|
||||
font-size: 24rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 50%;
|
||||
background: #e8f1ff;
|
||||
color: #1456c2;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.small-avatar {
|
||||
width: 66rpx;
|
||||
height: 66rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.large-avatar {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
font-size: 34rpx;
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
display: block;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.large-avatar-text {
|
||||
font-size: 34rpx;
|
||||
}
|
||||
|
||||
.scan-btn {
|
||||
width: 100%;
|
||||
height: 112rpx;
|
||||
line-height: 112rpx;
|
||||
margin: 0 0 28rpx;
|
||||
padding: 0;
|
||||
border-radius: 18rpx;
|
||||
background: linear-gradient(180deg, #2876ff 0%, #1456c2 100%);
|
||||
color: #ffffff;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
box-shadow:
|
||||
0 12rpx 0 #0d3f93,
|
||||
0 20rpx 30rpx rgba(20, 86, 194, 0.28),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.26);
|
||||
}
|
||||
|
||||
.scan-btn:active {
|
||||
transform: translateY(5rpx);
|
||||
box-shadow:
|
||||
0 7rpx 0 #0d3f93,
|
||||
0 14rpx 22rpx rgba(20, 86, 194, 0.22),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.scan-btn::after {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.notice-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 28rpx;
|
||||
border: 1rpx solid #dce5f2;
|
||||
border-radius: 18rpx;
|
||||
background: #ffffff;
|
||||
box-shadow:
|
||||
0 14rpx 0 rgba(205, 216, 229, 0.62),
|
||||
0 22rpx 34rpx rgba(22, 34, 50, 0.12),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.notice-card::before,
|
||||
.workbench-panel::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 18rpx;
|
||||
top: 18rpx;
|
||||
z-index: 1;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
border-radius: 50%;
|
||||
opacity: 0.54;
|
||||
pointer-events: none;
|
||||
background:
|
||||
linear-gradient(90deg, transparent 42%, rgba(47, 55, 65, 0.78) 43%, rgba(47, 55, 65, 0.78) 57%, transparent 58%),
|
||||
radial-gradient(circle at 32% 28%, #ffffff 0, #dfe6ec 19%, #97a3af 58%, #475467 100%);
|
||||
box-shadow:
|
||||
0 3rpx 0 rgba(72, 83, 97, 0.38),
|
||||
0 6rpx 10rpx rgba(22, 34, 50, 0.14),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.85);
|
||||
transform: rotate(28deg);
|
||||
}
|
||||
|
||||
.notice-hero {
|
||||
position: relative;
|
||||
min-height: 178rpx;
|
||||
padding: 24rpx 28rpx;
|
||||
background: linear-gradient(135deg, #1456c2 0%, #15847c 100%);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.notice-hero::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -14rpx;
|
||||
bottom: 14rpx;
|
||||
width: 180rpx;
|
||||
height: 48rpx;
|
||||
opacity: 0.14;
|
||||
transform: rotate(-20deg);
|
||||
pointer-events: none;
|
||||
background:
|
||||
radial-gradient(circle at 18% 50%, transparent 0rpx, transparent 12rpx, #ffffff 13rpx, #ffffff 20rpx, transparent 21rpx),
|
||||
radial-gradient(circle at 86% 50%, #ffffff 0rpx, #ffffff 16rpx, transparent 17rpx),
|
||||
linear-gradient(90deg, transparent 0rpx, transparent 30rpx, #ffffff 31rpx, #ffffff 142rpx, transparent 143rpx);
|
||||
}
|
||||
|
||||
.notice-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 42rpx;
|
||||
padding: 0 16rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
color: #ffffff;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.notice-swiper {
|
||||
height: 106rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.notice-slide,
|
||||
.notice-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.notice-title {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
color: #ffffff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.notice-content {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
margin-top: 8rpx;
|
||||
color: rgba(255, 255, 255, 0.86);
|
||||
font-size: 24rpx;
|
||||
line-height: 1.45;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.notice-list {
|
||||
padding: 16rpx 26rpx 18rpx;
|
||||
}
|
||||
|
||||
.notice-list-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
min-height: 46rpx;
|
||||
}
|
||||
|
||||
.notice-dot {
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
border-radius: 50%;
|
||||
background: #12a375;
|
||||
}
|
||||
|
||||
.notice-list-title {
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
color: #344054;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.trend-card {
|
||||
padding-bottom: 14rpx;
|
||||
border-left: 6rpx solid #12a375;
|
||||
}
|
||||
|
||||
.trend-head {
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.trend-plot {
|
||||
position: relative;
|
||||
height: 360rpx;
|
||||
padding: 18rpx 8rpx 0 58rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.trend-axis {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 48rpx;
|
||||
color: #667085;
|
||||
font-size: 20rpx;
|
||||
line-height: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.trend-axis-top {
|
||||
top: 24rpx;
|
||||
}
|
||||
|
||||
.trend-axis-bottom {
|
||||
bottom: 56rpx;
|
||||
}
|
||||
|
||||
.trend-plot-body {
|
||||
position: relative;
|
||||
height: 244rpx;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.trend-grid-line {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1rpx;
|
||||
background: #e6eaf0;
|
||||
}
|
||||
|
||||
.trend-grid-line.top {
|
||||
top: 14%;
|
||||
}
|
||||
|
||||
.trend-grid-line.middle {
|
||||
top: 48%;
|
||||
}
|
||||
|
||||
.trend-grid-line.bottom {
|
||||
top: 82%;
|
||||
}
|
||||
|
||||
.trend-segment {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
height: 6rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(180deg, #6aa1ff 0%, #1463ff 100%);
|
||||
box-shadow: 0 5rpx 10rpx rgba(20, 99, 255, 0.22);
|
||||
transform-origin: 0 50%;
|
||||
}
|
||||
|
||||
.trend-point-wrap {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
width: 1rpx;
|
||||
height: 1rpx;
|
||||
}
|
||||
|
||||
.trend-point {
|
||||
position: absolute;
|
||||
left: -11rpx;
|
||||
top: -11rpx;
|
||||
width: 22rpx;
|
||||
height: 22rpx;
|
||||
border: 5rpx solid #1463ff;
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
box-shadow:
|
||||
0 7rpx 12rpx rgba(20, 99, 255, 0.2),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.9);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.trend-point-value {
|
||||
position: absolute;
|
||||
left: -44rpx;
|
||||
bottom: 18rpx;
|
||||
width: 88rpx;
|
||||
color: #1f2933;
|
||||
font-size: 21rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.trend-label-row {
|
||||
position: relative;
|
||||
height: 40rpx;
|
||||
margin-top: 24rpx;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.trend-date-label {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 78rpx;
|
||||
color: #667085;
|
||||
font-size: 20rpx;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.tappable-stat {
|
||||
position: relative;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.tappable-stat:active {
|
||||
background: #f3f6fb;
|
||||
}
|
||||
|
||||
.stat-products {
|
||||
border-left-color: #1463ff;
|
||||
}
|
||||
|
||||
.stat-people {
|
||||
border-left-color: #12a375;
|
||||
}
|
||||
|
||||
.stat-pending {
|
||||
border-left-color: #d9822b;
|
||||
}
|
||||
|
||||
.stat-alert {
|
||||
border-color: #ffb4a8;
|
||||
border-left-color: #d92d20;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(217, 45, 32, 0.13), rgba(255, 255, 255, 0.92) 48%),
|
||||
#fff7f5;
|
||||
box-shadow:
|
||||
0 14rpx 0 rgba(244, 179, 169, 0.7),
|
||||
0 22rpx 34rpx rgba(180, 35, 24, 0.14),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.stat-alert .stat-number,
|
||||
.stat-alert .stat-link {
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
.stat-approved {
|
||||
border-left-color: #7c3aed;
|
||||
}
|
||||
|
||||
.stat-link {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
color: #6b778c;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
padding-bottom: 28rpx;
|
||||
border-left: 6rpx solid #12a375;
|
||||
}
|
||||
|
||||
.profile-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.profile-main {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
display: block;
|
||||
color: #1f2933;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 72rpx;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.inline-label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
color: #1f2933;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.role-list {
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
|
||||
.role-btn {
|
||||
margin-top: 12rpx;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
border-radius: 14rpx;
|
||||
background: linear-gradient(180deg, #f6faff 0%, #e4efff 100%);
|
||||
color: #1463ff;
|
||||
font-size: 26rpx;
|
||||
box-shadow:
|
||||
0 7rpx 0 rgba(144, 178, 235, 0.62),
|
||||
0 12rpx 18rpx rgba(20, 86, 194, 0.1),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.84);
|
||||
}
|
||||
|
||||
.role-btn::after {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.workbench-panel {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 24rpx 18rpx 10rpx;
|
||||
border: 1rpx solid #e4e9f1;
|
||||
border-radius: 18rpx;
|
||||
background: #ffffff;
|
||||
box-shadow:
|
||||
0 14rpx 0 rgba(205, 216, 229, 0.62),
|
||||
0 22rpx 34rpx rgba(22, 34, 50, 0.12),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.app-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 18rpx 12rpx;
|
||||
}
|
||||
|
||||
.app-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-height: 178rpx;
|
||||
padding: 16rpx 8rpx;
|
||||
border-radius: 16rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.app-item:active {
|
||||
background: #f3f6fb;
|
||||
}
|
||||
|
||||
.app-item:active .app-icon {
|
||||
transform: translateY(4rpx);
|
||||
box-shadow:
|
||||
0 5rpx 0 rgba(74, 91, 115, 0.42),
|
||||
0 8rpx 14rpx rgba(22, 34, 50, 0.12),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.56),
|
||||
inset 0 -8rpx 12rpx rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 78rpx;
|
||||
height: 78rpx;
|
||||
border-radius: 22rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.62);
|
||||
background: linear-gradient(160deg, #ffffff 0%, #dbe8ff 28%, #8bb7ff 58%, #1456c2 100%);
|
||||
color: #1456c2;
|
||||
font-size: 34rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
text-shadow:
|
||||
0 2rpx 0 rgba(255, 255, 255, 0.82),
|
||||
0 4rpx 8rpx rgba(15, 23, 42, 0.18);
|
||||
box-shadow:
|
||||
0 10rpx 0 rgba(77, 103, 145, 0.42),
|
||||
0 16rpx 22rpx rgba(22, 34, 50, 0.16),
|
||||
inset 0 3rpx 0 rgba(255, 255, 255, 0.82),
|
||||
inset 0 -10rpx 14rpx rgba(15, 23, 42, 0.14);
|
||||
}
|
||||
|
||||
.app-icon::before,
|
||||
.app-icon::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
background:
|
||||
linear-gradient(90deg, transparent 40%, rgba(47, 55, 65, 0.72) 41%, rgba(47, 55, 65, 0.72) 59%, transparent 60%),
|
||||
radial-gradient(circle at 32% 28%, rgba(255, 255, 255, 0.96) 0, rgba(226, 232, 240, 0.96) 20%, rgba(132, 146, 164, 0.88) 62%, rgba(59, 70, 84, 0.86) 100%);
|
||||
box-shadow:
|
||||
0 2rpx 0 rgba(49, 61, 77, 0.36),
|
||||
inset 0 1rpx 0 rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.app-icon::before {
|
||||
left: 10rpx;
|
||||
top: 10rpx;
|
||||
transform: rotate(24deg);
|
||||
}
|
||||
|
||||
.app-icon::after {
|
||||
right: 10rpx;
|
||||
bottom: 10rpx;
|
||||
transform: rotate(-28deg);
|
||||
}
|
||||
|
||||
.app-icon-products,
|
||||
.app-icon-device,
|
||||
.app-icon-moldQr,
|
||||
.app-icon-equipment {
|
||||
background: linear-gradient(160deg, #ffffff 0%, #dbe8ff 28%, #8bb7ff 58%, #1456c2 100%);
|
||||
color: #1456c2;
|
||||
}
|
||||
|
||||
.app-icon-review,
|
||||
.app-icon-audit {
|
||||
background: linear-gradient(160deg, #fffdf7 0%, #ffe2aa 34%, #f0a23a 68%, #a95605 100%);
|
||||
color: #a95605;
|
||||
box-shadow:
|
||||
0 10rpx 0 rgba(143, 84, 16, 0.38),
|
||||
0 16rpx 22rpx rgba(104, 62, 17, 0.14),
|
||||
inset 0 3rpx 0 rgba(255, 255, 255, 0.82),
|
||||
inset 0 -10rpx 14rpx rgba(94, 47, 0, 0.14);
|
||||
}
|
||||
|
||||
.app-icon-people,
|
||||
.app-icon-dashboard,
|
||||
.app-icon-schedule {
|
||||
background: linear-gradient(160deg, #ffffff 0%, #c9f2df 32%, #5fd19d 64%, #087443 100%);
|
||||
color: #087443;
|
||||
box-shadow:
|
||||
0 10rpx 0 rgba(24, 117, 77, 0.38),
|
||||
0 16rpx 22rpx rgba(20, 92, 63, 0.14),
|
||||
inset 0 3rpx 0 rgba(255, 255, 255, 0.82),
|
||||
inset 0 -10rpx 14rpx rgba(0, 81, 49, 0.14);
|
||||
}
|
||||
|
||||
.app-icon-records,
|
||||
.app-icon-notices {
|
||||
background: linear-gradient(160deg, #ffffff 0%, #e6d8ff 32%, #b68dff 64%, #6d35c7 100%);
|
||||
color: #6d35c7;
|
||||
box-shadow:
|
||||
0 10rpx 0 rgba(98, 67, 151, 0.38),
|
||||
0 16rpx 22rpx rgba(81, 54, 125, 0.14),
|
||||
inset 0 3rpx 0 rgba(255, 255, 255, 0.82),
|
||||
inset 0 -10rpx 14rpx rgba(65, 29, 122, 0.14);
|
||||
}
|
||||
|
||||
.app-title {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
color: #162232;
|
||||
font-size: 25rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.app-desc {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
margin-top: 6rpx;
|
||||
color: #7a8699;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.35;
|
||||
text-align: center;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 20;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
padding: 10rpx 22rpx calc(env(safe-area-inset-bottom) + 10rpx);
|
||||
border-top: 1rpx solid #e4e9f1;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f6f9fd 100%);
|
||||
box-shadow:
|
||||
0 -8rpx 0 rgba(205, 216, 229, 0.42),
|
||||
0 -14rpx 28rpx rgba(22, 34, 50, 0.1),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.bottom-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 94rpx;
|
||||
color: #8a94a6;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 18rpx;
|
||||
}
|
||||
|
||||
.bottom-item.active {
|
||||
background: linear-gradient(180deg, #eef5ff 0%, #dceaff 100%);
|
||||
color: #1463ff;
|
||||
box-shadow:
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.95),
|
||||
0 7rpx 0 rgba(144, 178, 235, 0.46),
|
||||
0 12rpx 20rpx rgba(20, 86, 194, 0.14);
|
||||
}
|
||||
|
||||
.bottom-item.active text:not(.bottom-icon) {
|
||||
color: #1463ff;
|
||||
font-size: 24rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.bottom-item text:not(.bottom-icon) {
|
||||
color: #7a8699;
|
||||
}
|
||||
|
||||
.bottom-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 46rpx;
|
||||
height: 46rpx;
|
||||
margin-bottom: 6rpx;
|
||||
border: 1rpx solid #c8d2df;
|
||||
border-radius: 16rpx;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #e7edf5 100%);
|
||||
color: #8b96a8;
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
box-shadow:
|
||||
0 5rpx 0 rgba(157, 170, 188, 0.4),
|
||||
0 8rpx 12rpx rgba(22, 34, 50, 0.08),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
.bottom-item.active .bottom-icon {
|
||||
width: 58rpx;
|
||||
height: 58rpx;
|
||||
margin-bottom: 4rpx;
|
||||
border-color: rgba(255, 255, 255, 0.72);
|
||||
background: linear-gradient(160deg, #ffffff 0%, #dbe8ff 28%, #72a9ff 58%, #1456c2 100%);
|
||||
color: #ffffff;
|
||||
font-size: 34rpx;
|
||||
text-shadow: 0 2rpx 4rpx rgba(15, 23, 42, 0.28);
|
||||
box-shadow:
|
||||
0 8rpx 0 #0d3f93,
|
||||
0 14rpx 20rpx rgba(20, 86, 194, 0.24),
|
||||
inset 0 3rpx 0 rgba(255, 255, 255, 0.78),
|
||||
inset 0 -8rpx 14rpx rgba(15, 23, 42, 0.16);
|
||||
}
|
||||
Reference in New Issue
Block a user