|
|
@@ -0,0 +1,555 @@
|
|
|
+<template>
|
|
|
+ <view class="page">
|
|
|
+ <!-- 导航栏 -->
|
|
|
+ <NavBar title="客户详情" :showBack="true" />
|
|
|
+
|
|
|
+ <!-- 客户信息卡片 -->
|
|
|
+ <view class="info-section">
|
|
|
+ <view class="info-card">
|
|
|
+ <view class="info-header">
|
|
|
+ <view class="info-avatar">
|
|
|
+ <text class="avatar-text">{{ getAvatarText(customer.nickname) }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-main">
|
|
|
+ <text class="customer-name">{{ customer.nickname || '未命名用户' }}</text>
|
|
|
+ <view class="status-tag" :class="getStatusClass(customer.status)">
|
|
|
+ <text>{{ getStatusText(customer.status) }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="info-grid">
|
|
|
+ <view class="info-item">
|
|
|
+ <text class="info-label">手机号</text>
|
|
|
+ <text class="info-value">{{ formatPhone(customer.phone) }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-item">
|
|
|
+ <text class="info-label">信用分</text>
|
|
|
+ <text class="info-value" :class="getCreditClass(customer.creditScore)">{{ customer.creditScore || 0 }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-item">
|
|
|
+ <text class="info-label">注册时间</text>
|
|
|
+ <text class="info-value">{{ formatDate(customer.createTime) }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-item">
|
|
|
+ <text class="info-label">用户ID</text>
|
|
|
+ <text class="info-value id">{{ customer.id }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 操作按钮 -->
|
|
|
+ <view class="action-section">
|
|
|
+ <view class="action-card">
|
|
|
+ <text class="action-title">账户操作</text>
|
|
|
+ <view class="action-list">
|
|
|
+ <view class="action-item" @click="handleToggleStatus">
|
|
|
+ <view class="action-icon" :class="customer.status === 1 ? 'warning' : 'success'">
|
|
|
+ <view class="icon-status"></view>
|
|
|
+ </view>
|
|
|
+ <view class="action-content">
|
|
|
+ <text class="action-name">{{ customer.status === 1 ? '禁用账户' : '启用账户' }}</text>
|
|
|
+ <text class="action-desc">{{ customer.status === 1 ? '禁止该用户下单' : '恢复该用户正常使用' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="action-arrow"></view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="action-item" @click="showCreditModal = true">
|
|
|
+ <view class="action-icon primary">
|
|
|
+ <view class="icon-credit"></view>
|
|
|
+ </view>
|
|
|
+ <view class="action-content">
|
|
|
+ <text class="action-name">调整信用分</text>
|
|
|
+ <text class="action-desc">修改用户信用评分</text>
|
|
|
+ </view>
|
|
|
+ <view class="action-arrow"></view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 加载状态 -->
|
|
|
+ <view class="loading-state" v-if="loading">
|
|
|
+ <view class="loading-spinner"></view>
|
|
|
+ <text>加载中...</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 信用分调整弹窗 -->
|
|
|
+ <view class="modal-overlay" v-if="showCreditModal" @click="showCreditModal = false">
|
|
|
+ <view class="modal-content" @click.stop>
|
|
|
+ <view class="modal-header">
|
|
|
+ <text class="modal-title">调整信用分</text>
|
|
|
+ </view>
|
|
|
+ <view class="modal-body">
|
|
|
+ <text class="modal-label">当前信用分: {{ customer.creditScore || 0 }}</text>
|
|
|
+ <input
|
|
|
+ class="modal-input"
|
|
|
+ v-model="newCreditScore"
|
|
|
+ type="number"
|
|
|
+ placeholder="请输入新的信用分"
|
|
|
+ placeholder-class="input-placeholder"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ <view class="modal-footer">
|
|
|
+ <button class="btn-cancel" @click="showCreditModal = false">取消</button>
|
|
|
+ <button class="btn-confirm" @click="handleUpdateCredit">确认</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, onMounted } from 'vue';
|
|
|
+import NavBar from '@/components/NavBar.vue';
|
|
|
+import { getCustomerDetail, updateCustomerStatus, updateCustomerCredit } from '@/api/customer';
|
|
|
+import { UserStatus, UserStatusText } from '@/utils/constants';
|
|
|
+import { showConfirm, showToast } from '@/utils/common';
|
|
|
+
|
|
|
+const customer = ref<any>({});
|
|
|
+const loading = ref(false);
|
|
|
+const showCreditModal = ref(false);
|
|
|
+const newCreditScore = ref('');
|
|
|
+
|
|
|
+const getStatusText = (status: number) => UserStatusText[status] || '未知';
|
|
|
+
|
|
|
+const getStatusClass = (status: number) => {
|
|
|
+ return status === UserStatus.ENABLED ? 'enabled' : 'disabled';
|
|
|
+};
|
|
|
+
|
|
|
+const getAvatarText = (nickname: string) => {
|
|
|
+ if (!nickname) return '?';
|
|
|
+ return nickname.charAt(0).toUpperCase();
|
|
|
+};
|
|
|
+
|
|
|
+const formatPhone = (phone: string) => {
|
|
|
+ if (!phone) return '未绑定手机号';
|
|
|
+ if (phone.length === 11) {
|
|
|
+ return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
|
|
|
+ }
|
|
|
+ return phone;
|
|
|
+};
|
|
|
+
|
|
|
+const formatDate = (dateStr: string) => {
|
|
|
+ if (!dateStr) return '-';
|
|
|
+ const date = new Date(dateStr);
|
|
|
+ if (isNaN(date.getTime())) return dateStr;
|
|
|
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
|
+};
|
|
|
+
|
|
|
+const getCreditClass = (score: number) => {
|
|
|
+ if (!score || score >= 80) return 'high';
|
|
|
+ if (score >= 60) return 'medium';
|
|
|
+ return 'low';
|
|
|
+};
|
|
|
+
|
|
|
+const loadCustomerDetail = async () => {
|
|
|
+ const pages = getCurrentPages();
|
|
|
+ const currentPage = pages[pages.length - 1];
|
|
|
+ const customerId = (currentPage as any).options?.id;
|
|
|
+ if (!customerId) {
|
|
|
+ showToast('客户ID不存在', 'error');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ const res = await getCustomerDetail(Number(customerId));
|
|
|
+ customer.value = res || {};
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载客户详情失败', error);
|
|
|
+ showToast('加载客户详情失败', 'error');
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const handleToggleStatus = async () => {
|
|
|
+ const newStatus = customer.value.status === UserStatus.ENABLED ? UserStatus.DISABLED : UserStatus.ENABLED;
|
|
|
+ const actionText = newStatus === UserStatus.DISABLED ? '禁用' : '启用';
|
|
|
+
|
|
|
+ const confirmed = await showConfirm(`确认${actionText}该客户账户?`);
|
|
|
+ if (!confirmed) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ await updateCustomerStatus(customer.value.id, newStatus);
|
|
|
+ customer.value.status = newStatus;
|
|
|
+ showToast(`${actionText}成功`, 'success');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('更新客户状态失败', error);
|
|
|
+ showToast('操作失败', 'error');
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const handleUpdateCredit = async () => {
|
|
|
+ const score = parseInt(newCreditScore.value);
|
|
|
+ if (isNaN(score) || score < 0 || score > 100) {
|
|
|
+ showToast('请输入0-100之间的有效分数', 'error');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await updateCustomerCredit(customer.value.id, score);
|
|
|
+ customer.value.creditScore = score;
|
|
|
+ showCreditModal.value = false;
|
|
|
+ newCreditScore.value = '';
|
|
|
+ showToast('信用分更新成功', 'success');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('更新信用分失败', error);
|
|
|
+ showToast('操作失败', 'error');
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ loadCustomerDetail();
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.page {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: #f8fafc;
|
|
|
+ padding-bottom: 40rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* 信息卡片 */
|
|
|
+.info-section {
|
|
|
+ padding: 16rpx 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.info-card {
|
|
|
+ background: #ffffff;
|
|
|
+ border: 1rpx solid #e2e8f0;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ padding: 32rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.info-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.info-avatar {
|
|
|
+ width: 100rpx;
|
|
|
+ height: 100rpx;
|
|
|
+ background: #ecfdf5;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ margin-right: 24rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
+
|
|
|
+ .avatar-text {
|
|
|
+ font-size: 40rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #10b981;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.info-main {
|
|
|
+ flex: 1;
|
|
|
+
|
|
|
+ .customer-name {
|
|
|
+ display: block;
|
|
|
+ font-size: 34rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #1e293b;
|
|
|
+ margin-bottom: 8rpx;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.status-tag {
|
|
|
+ display: inline-flex;
|
|
|
+ padding: 4rpx 12rpx;
|
|
|
+ border-radius: 6rpx;
|
|
|
+ font-size: 22rpx;
|
|
|
+ font-weight: 500;
|
|
|
+
|
|
|
+ &.enabled {
|
|
|
+ background: #ecfdf5;
|
|
|
+ color: #10b981;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.disabled {
|
|
|
+ background: #fef2f2;
|
|
|
+ color: #ef4444;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.info-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 20rpx;
|
|
|
+ background: #f8fafc;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ padding: 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.info-item {
|
|
|
+ .info-label {
|
|
|
+ display: block;
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #94a3b8;
|
|
|
+ margin-bottom: 4rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .info-value {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #1e293b;
|
|
|
+ font-weight: 500;
|
|
|
+
|
|
|
+ &.high { color: #10b981; }
|
|
|
+ &.medium { color: #f97316; }
|
|
|
+ &.low { color: #ef4444; }
|
|
|
+ &.id { font-size: 22rpx; color: #64748b; font-family: monospace; }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 操作区域 */
|
|
|
+.action-section {
|
|
|
+ padding: 0 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.action-card {
|
|
|
+ background: #ffffff;
|
|
|
+ border: 1rpx solid #e2e8f0;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ .action-title {
|
|
|
+ display: block;
|
|
|
+ padding: 20rpx 24rpx 12rpx;
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #94a3b8;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.action-list {
|
|
|
+ .action-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 20rpx 24rpx;
|
|
|
+ border-bottom: 1rpx solid #f1f5f9;
|
|
|
+ transition: background 0.15s;
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:active {
|
|
|
+ background: #f8fafc;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.action-icon {
|
|
|
+ width: 56rpx;
|
|
|
+ height: 56rpx;
|
|
|
+ border-radius: 14rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ margin-right: 16rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
+
|
|
|
+ &.warning { background: #fff7ed; }
|
|
|
+ &.success { background: #ecfdf5; }
|
|
|
+ &.primary { background: #f0f9ff; }
|
|
|
+
|
|
|
+ .icon-status {
|
|
|
+ width: 20rpx;
|
|
|
+ height: 20rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ border: 3rpx solid #f97316;
|
|
|
+ position: relative;
|
|
|
+
|
|
|
+ &::after {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ width: 8rpx;
|
|
|
+ height: 8rpx;
|
|
|
+ background: #f97316;
|
|
|
+ border-radius: 50%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .icon-credit {
|
|
|
+ width: 20rpx;
|
|
|
+ height: 20rpx;
|
|
|
+ border: 3rpx solid #0ea5e9;
|
|
|
+ border-radius: 50%;
|
|
|
+ position: relative;
|
|
|
+
|
|
|
+ &::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ width: 3rpx;
|
|
|
+ height: 10rpx;
|
|
|
+ background: #0ea5e9;
|
|
|
+ }
|
|
|
+
|
|
|
+ &::after {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ top: 4rpx;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ width: 3rpx;
|
|
|
+ height: 3rpx;
|
|
|
+ background: #0ea5e9;
|
|
|
+ border-radius: 50%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.action-content {
|
|
|
+ flex: 1;
|
|
|
+
|
|
|
+ .action-name {
|
|
|
+ display: block;
|
|
|
+ font-size: 30rpx;
|
|
|
+ color: #1e293b;
|
|
|
+ font-weight: 500;
|
|
|
+ margin-bottom: 2rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .action-desc {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #94a3b8;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.action-arrow {
|
|
|
+ width: 12rpx;
|
|
|
+ height: 12rpx;
|
|
|
+ border-top: 3rpx solid #cbd5e1;
|
|
|
+ border-right: 3rpx solid #cbd5e1;
|
|
|
+ transform: rotate(45deg);
|
|
|
+ margin-left: 12rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 加载状态 */
|
|
|
+.loading-state {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 100rpx 0;
|
|
|
+ color: #94a3b8;
|
|
|
+ font-size: 28rpx;
|
|
|
+
|
|
|
+ .loading-spinner {
|
|
|
+ width: 48rpx;
|
|
|
+ height: 48rpx;
|
|
|
+ border: 4rpx solid #e2e8f0;
|
|
|
+ border-top-color: #10b981;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes spin {
|
|
|
+ to { transform: rotate(360deg); }
|
|
|
+}
|
|
|
+
|
|
|
+/* 弹窗 */
|
|
|
+.modal-overlay {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
+ z-index: 100;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 0 48rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.modal-content {
|
|
|
+ width: 100%;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ .modal-header {
|
|
|
+ padding: 24rpx;
|
|
|
+ border-bottom: 1rpx solid #f1f5f9;
|
|
|
+
|
|
|
+ .modal-title {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1e293b;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal-body {
|
|
|
+ padding: 24rpx;
|
|
|
+
|
|
|
+ .modal-label {
|
|
|
+ display: block;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #64748b;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal-input {
|
|
|
+ width: 100%;
|
|
|
+ height: 88rpx;
|
|
|
+ background: #f8fafc;
|
|
|
+ border: 2rpx solid #e2e8f0;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ padding: 0 20rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #1e293b;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal-footer {
|
|
|
+ display: flex;
|
|
|
+ padding: 16rpx 24rpx 24rpx;
|
|
|
+ gap: 12rpx;
|
|
|
+
|
|
|
+ button {
|
|
|
+ flex: 1;
|
|
|
+ height: 80rpx;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ border: none;
|
|
|
+
|
|
|
+ &::after {
|
|
|
+ border: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-cancel {
|
|
|
+ background: #f8fafc;
|
|
|
+ color: #64748b;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-confirm {
|
|
|
+ background: #10b981;
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.input-placeholder {
|
|
|
+ color: #cbd5e1;
|
|
|
+}
|
|
|
+</style>
|