petstore-frontend/src/pages/mine/Staff.vue

393 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>