393 lines
12 KiB
Vue
393 lines
12 KiB
Vue
<template>
|
||
<view class="page-shell staff-page">
|
||
<view class="staff-nav nav-gradient" :style="navSafeStyle">
|
||
<view class="nav-back" @click="goBackToMine"><AppIcon name="back" :size="18" color="#ffffff" /></view>
|
||
<view class="nav-title">员工管理</view>
|
||
<view class="nav-placeholder"></view>
|
||
</view>
|
||
|
||
<!-- 邀请码:标题与码+按钮分行,同一行内码与复制垂直居中 -->
|
||
<view class="page-section invite-wrap">
|
||
<text class="invite-kicker">员工邀请码</text>
|
||
<view class="invite-code-row">
|
||
<text class="invite-code-text" selectable>{{ inviteCodeText }}</text>
|
||
<button class="invite-copy-btn" hover-class="invite-copy-btn--hover" @click="copyCode">复制</button>
|
||
</view>
|
||
<text class="invite-tip">新员工注册时填写此码即可加入本店</text>
|
||
</view>
|
||
|
||
<view class="page-section add-btn-wrap">
|
||
<button class="van-button van-button--primary van-button--block" @click="showAddStaff = true">新增员工</button>
|
||
</view>
|
||
|
||
<!-- 员工列表 -->
|
||
<view class="page-section section-gap">
|
||
<view class="van-cell-group card-section">
|
||
<view
|
||
v-for="(s, idx) in staffList"
|
||
:key="s.id != null ? s.id : 'row-' + idx"
|
||
class="staff-swipe-wrap"
|
||
:class="{ 'is-open': swipedId === s.id && s.role !== 'boss' }"
|
||
@click="onRowTap(s)"
|
||
>
|
||
<view
|
||
v-show="s.role !== 'boss'"
|
||
class="swipe-delete-btn"
|
||
@click.stop="deleteStaff(s.id)"
|
||
>
|
||
删除
|
||
</view>
|
||
<view
|
||
class="staff-main"
|
||
@touchstart="onSwipeStart($event, s)"
|
||
@touchmove.stop="onSwipeMove($event)"
|
||
@touchend="onSwipeEnd($event, s)"
|
||
>
|
||
<view class="van-cell staff-cell">
|
||
<view class="staff-info-row">
|
||
<view class="avatar" :style="avatarStyle(s)">
|
||
<image v-if="s.avatar" :src="s.avatar" class="avatar-img" mode="aspectFill" />
|
||
<text v-else class="avatar-initials">{{ staffInitial(s) }}</text>
|
||
</view>
|
||
<view>
|
||
<view class="staff-name">{{ s.name }}</view>
|
||
<view class="staff-meta">{{ s.phone }}</view>
|
||
<view class="staff-meta">{{ s.role === 'boss' ? '店长' : '员工' }}</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="staffList.length === 0" class="empty"><text>暂无员工</text></view>
|
||
|
||
<view v-if="showAddStaff" class="popup-mask" @click="showAddStaff = false">
|
||
<view class="popup-content" @click.stop>
|
||
<view class="popup-header">
|
||
<view class="popup-title">新增员工</view>
|
||
<view class="popup-close" @click="showAddStaff = false">✕</view>
|
||
</view>
|
||
<view class="popup-body">
|
||
<view class="popup-desc">创建后会自动生成初始密码,员工可使用手机号登录。</view>
|
||
<view class="field-label">员工姓名</view>
|
||
<input v-model="newStaff.name" class="van-field" placeholder="请输入" />
|
||
<view class="field-label">手机号</view>
|
||
<input v-model="newStaff.phone" type="tel" class="van-field" placeholder="请输入" maxlength="11" />
|
||
</view>
|
||
<view class="popup-footer">
|
||
<view class="popup-actions">
|
||
<button class="van-button btn-ghost" @click="showAddStaff = false">取消</button>
|
||
<button class="van-button van-button--primary btn-confirm" @click="confirmAddStaff">确认创建</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import AppIcon from '../../components/AppIcon.vue'
|
||
import { openAppPage } from '../../utils/globalState.js'
|
||
import { getStaffList, createStaff, deleteStaff as delStaff } from '../../api/index.js'
|
||
import { getStoreSession } from '../../utils/session.js'
|
||
|
||
const emit = defineEmits(['change-page'])
|
||
const storeInfo = getStoreSession()
|
||
const inviteCodeText = computed(() => (storeInfo && storeInfo.inviteCode) ? String(storeInfo.inviteCode) : '—')
|
||
|
||
const navSafeStyle = (() => {
|
||
let statusBarHeight = 20
|
||
try {
|
||
const win = typeof uni.getWindowInfo === 'function' ? uni.getWindowInfo() : null
|
||
if (win && win.statusBarHeight != null) statusBarHeight = win.statusBarHeight
|
||
else {
|
||
const sys = uni.getSystemInfoSync()
|
||
statusBarHeight = sys.statusBarHeight || 20
|
||
}
|
||
} catch (_) {
|
||
statusBarHeight = 20
|
||
}
|
||
let navHeight = statusBarHeight + 44
|
||
const menuRect = uni.getMenuButtonBoundingClientRect?.()
|
||
if (menuRect && menuRect.top && menuRect.height) {
|
||
const verticalGap = Math.max(menuRect.top - statusBarHeight, 4)
|
||
navHeight = statusBarHeight + verticalGap * 2 + menuRect.height
|
||
}
|
||
return `padding-top:${statusBarHeight}px;height:${navHeight}px;`
|
||
})()
|
||
|
||
const goBackToMine = () => {
|
||
const pages = getCurrentPages()
|
||
if (pages.length > 1) {
|
||
uni.navigateBack()
|
||
return
|
||
}
|
||
openAppPage('mine')
|
||
}
|
||
|
||
const staffInitial = (s) => {
|
||
const n = s && s.name
|
||
return (typeof n === 'string' && n.length > 0) ? n[0] : '?'
|
||
}
|
||
|
||
const staffList = ref([])
|
||
const showAddStaff = ref(false)
|
||
const newStaff = ref({ name: '', phone: '' })
|
||
const swipedId = ref(null)
|
||
const touchStartX = ref(0)
|
||
const touchCurrentX = ref(0)
|
||
|
||
const COLORS = ['#f5913e', '#2db96d', '#8b6914', '#e06050', '#5090d0', '#9b59b6']
|
||
const avatarStyle = (s) => {
|
||
if (s.avatar) return { background: 'transparent' }
|
||
const idx = (s.name?.charCodeAt(0) || 0) % COLORS.length
|
||
return { background: COLORS[idx] }
|
||
}
|
||
|
||
const loadStaff = async () => {
|
||
const res = await getStaffList(storeInfo.id)
|
||
if (res.code === 200) staffList.value = res.data
|
||
}
|
||
|
||
const copyCode = () => {
|
||
const code = inviteCodeText.value === '—' ? '' : inviteCodeText.value
|
||
if (!code) return
|
||
uni.setClipboardData({
|
||
data: code,
|
||
success: () => uni.showToast({ title: '邀请码已复制', icon: 'none' })
|
||
})
|
||
}
|
||
|
||
const confirmAddStaff = async () => {
|
||
if (!newStaff.value.name) { uni.showToast({ title: '请输入员工姓名', icon: 'none' }); return }
|
||
if (!newStaff.value.phone || newStaff.value.phone.length !== 11) { uni.showToast({ title: '请输入正确的手机号', icon: 'none' }); return }
|
||
const res = await createStaff({ storeId: storeInfo.id, name: newStaff.value.name, phone: newStaff.value.phone })
|
||
if (res.code === 200) {
|
||
uni.showToast({ title: `添加成功,密码:${res.data.password}`, icon: 'none', duration: 3000 })
|
||
showAddStaff.value = false
|
||
newStaff.value = { name: '', phone: '' }
|
||
loadStaff()
|
||
} else {
|
||
uni.showToast({ title: res.message || '添加失败', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
const getTouchX = (e) => {
|
||
if (e?.touches?.[0]) return e.touches[0].pageX
|
||
if (e?.changedTouches?.[0]) return e.changedTouches[0].pageX
|
||
return 0
|
||
}
|
||
|
||
const onSwipeStart = (e, staff) => {
|
||
const x = getTouchX(e)
|
||
touchStartX.value = x
|
||
touchCurrentX.value = x
|
||
if (swipedId.value && swipedId.value !== staff.id) {
|
||
swipedId.value = null
|
||
}
|
||
}
|
||
|
||
const onSwipeMove = (e) => {
|
||
touchCurrentX.value = getTouchX(e)
|
||
}
|
||
|
||
const onSwipeEnd = (e, staff) => {
|
||
if (staff.role === 'boss') return
|
||
const endX = getTouchX(e) || touchCurrentX.value
|
||
const deltaX = endX - touchStartX.value
|
||
if (deltaX < -40) {
|
||
swipedId.value = staff.id
|
||
return
|
||
}
|
||
if (deltaX > 24 && swipedId.value === staff.id) {
|
||
swipedId.value = null
|
||
}
|
||
}
|
||
|
||
const onRowTap = (staff) => {
|
||
if (swipedId.value && swipedId.value !== staff.id) {
|
||
swipedId.value = null
|
||
}
|
||
}
|
||
|
||
const deleteStaff = async (staffId) => {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '确定删除该员工?',
|
||
success: async (res) => {
|
||
if (!res.confirm) return
|
||
const r = await delStaff(staffId)
|
||
if (r.code === 200) {
|
||
swipedId.value = null
|
||
uni.showToast({ title: '已删除', icon: 'success' })
|
||
loadStaff()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
onMounted(() => loadStaff())
|
||
</script>
|
||
|
||
<style scoped>
|
||
.staff-page { padding-bottom: 120rpx; }
|
||
.nav-placeholder { width: 32px; }
|
||
.add-btn-wrap { padding: 0; }
|
||
.card-section { margin: 0; }
|
||
.staff-cell { min-height: 58px; }
|
||
|
||
/* 邀请码卡片 */
|
||
.invite-wrap {
|
||
background: #fff;
|
||
border-radius: 14px;
|
||
padding: 16px 16px 14px;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||
border: 1px solid #f0f0ee;
|
||
}
|
||
.invite-kicker {
|
||
display: block;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
color: #999993;
|
||
letter-spacing: 0.5px;
|
||
margin-bottom: 10px;
|
||
}
|
||
.invite-code-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
min-height: 44px;
|
||
}
|
||
.invite-code-text {
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
font-family: ui-monospace, Menlo, Monaco, Consolas, monospace;
|
||
color: #1a1a1a;
|
||
letter-spacing: 0.12em;
|
||
line-height: 1.3;
|
||
}
|
||
.invite-copy-btn {
|
||
flex-shrink: 0;
|
||
margin: 0;
|
||
padding: 0 18px;
|
||
height: 36px;
|
||
line-height: 36px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #2db96d;
|
||
background: #eaf8f0;
|
||
border: none;
|
||
border-radius: 10px;
|
||
box-sizing: border-box;
|
||
}
|
||
.invite-copy-btn::after {
|
||
border: none;
|
||
}
|
||
.invite-copy-btn--hover {
|
||
opacity: 0.88;
|
||
}
|
||
.invite-tip {
|
||
display: block;
|
||
margin-top: 10px;
|
||
font-size: 12px;
|
||
color: #bbbbb5;
|
||
line-height: 1.4;
|
||
}
|
||
.staff-info-row { display: flex; align-items: center; gap: 12px; flex: 1; }
|
||
.staff-name { font-weight: 700; font-size: 15px; color: #1f2937; }
|
||
.staff-meta { font-size: 12px; color: #6b7280; }
|
||
.staff-swipe-wrap {
|
||
position: relative;
|
||
overflow: hidden;
|
||
border-radius: 12px;
|
||
background: #fff;
|
||
}
|
||
.staff-swipe-wrap + .staff-swipe-wrap {
|
||
border-top: 1px solid #eef2f7;
|
||
}
|
||
.staff-main {
|
||
position: relative;
|
||
z-index: 2;
|
||
transform: translateX(0);
|
||
transition: transform 0.2s ease;
|
||
background: #fff;
|
||
width: 100%;
|
||
min-width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
.staff-swipe-wrap.is-open .staff-main {
|
||
transform: translateX(-76px);
|
||
}
|
||
.swipe-delete-btn {
|
||
position: absolute;
|
||
right: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 76px;
|
||
background: #ef4444;
|
||
color: #fff;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
flex-shrink: 0;
|
||
}
|
||
.staff-nav {
|
||
padding: 14px 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
}
|
||
.nav-back { font-size: 20px; color: #fff; }
|
||
.nav-title { font-size: 18px; font-weight: 700; color: #fff; }
|
||
.avatar {
|
||
width: 42px; height: 42px;
|
||
border-radius: 50%;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 18px; color: #fff;
|
||
overflow: hidden;
|
||
flex-shrink: 0;
|
||
border: 1px solid #e8edf4;
|
||
}
|
||
.avatar-img { width: 100%; height: 100%; object-fit: cover; }
|
||
.avatar-initials { font-weight: 600; }
|
||
.popup-mask {
|
||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||
background: rgba(0,0,0,0.5); z-index: 100;
|
||
display: flex; align-items: flex-end; justify-content: center;
|
||
}
|
||
.popup-content {
|
||
background: #fff; border-radius: 16px 16px 0 0;
|
||
width: 430px; max-width: 100%;
|
||
display: flex; flex-direction: column;
|
||
}
|
||
.popup-header {
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
padding: 16px 20px; border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
.popup-title { font-size: 16px; font-weight: 600; }
|
||
.popup-close { font-size: 18px; color: #999; }
|
||
.popup-body { padding: 16px 20px; }
|
||
.popup-footer { padding: 12px 20px 20px; }
|
||
.popup-desc { font-size: 12px; color: #6b7280; line-height: 1.45; margin-bottom: 10px; }
|
||
.popup-actions { display: flex; align-items: center; gap: 10px; }
|
||
.btn-ghost {
|
||
width: 96px;
|
||
border: 1px solid #dbe3ee !important;
|
||
background: #fff !important;
|
||
color: #64748b !important;
|
||
}
|
||
.btn-confirm { flex: 1; }
|
||
</style>
|