|
@@ -25,8 +25,11 @@
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
<view class="item-header-right">
|
|
<view class="item-header-right">
|
|
|
- <text class="status-tag" :style="getStatusStyle(item.status)">{{ fmtDictName('User.status', item.status) }}</text>
|
|
|
|
|
|
|
+ <text v-if="item.lockNotice" class="status-tag locked-tag">已锁定</text>
|
|
|
|
|
+ <text v-else class="status-tag" :style="getStatusStyle(item.status)">{{ fmtDictName('User.status', item.status) }}</text>
|
|
|
<button class="refund-btn" @click.stop="handleApplyRefund(item)">退款</button>
|
|
<button class="refund-btn" @click.stop="handleApplyRefund(item)">退款</button>
|
|
|
|
|
+ <button v-if="!item.lockNotice" class="lock-btn" @click.stop="openLockModal(item)">锁定</button>
|
|
|
|
|
+ <button v-else class="unlock-btn" @click.stop="handleUnlock(item)">解锁</button>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
<view class="item-content">
|
|
<view class="item-content">
|
|
@@ -61,6 +64,24 @@
|
|
|
<text class="info-value">{{ formatTime(item.registerTime) }}</text>
|
|
<text class="info-value">{{ formatTime(item.registerTime) }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+ <!-- 锁定信息(仅平台人员可见) -->
|
|
|
|
|
+ <view class="lock-info">
|
|
|
|
|
+ <view class="info-item">
|
|
|
|
|
+ <text class="info-label">锁定次数</text>
|
|
|
|
|
+ <text v-if="item.lockTimes > 0" class="lock-count-link" @click.stop="openLockRecordModal(item)">
|
|
|
|
|
+ {{ item.lockTimes }} 次 ›
|
|
|
|
|
+ </text>
|
|
|
|
|
+ <text v-else class="info-value">0 次</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="info-item" v-if="item.lockNotice">
|
|
|
|
|
+ <text class="info-label">锁定备注</text>
|
|
|
|
|
+ <text class="info-value">{{ item.lockRemark || '-' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="info-item" v-if="item.lockNotice">
|
|
|
|
|
+ <text class="info-label">锁定时间</text>
|
|
|
|
|
+ <text class="info-value">{{ formatTime(item.lockTime) }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
<view class="stats-row">
|
|
<view class="stats-row">
|
|
|
<view class="stat-item">
|
|
<view class="stat-item">
|
|
|
<text class="stat-num">{{ item.rechargeTimes || 0 }}</text>
|
|
<text class="stat-num">{{ item.rechargeTimes || 0 }}</text>
|
|
@@ -122,13 +143,86 @@
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 锁定用户弹窗 -->
|
|
|
|
|
+ <view class="modal-overlay" v-if="showLockModal" @click="closeLockModal">
|
|
|
|
|
+ <view class="modal-card" @click.stop>
|
|
|
|
|
+ <text class="modal-title">锁定用户</text>
|
|
|
|
|
+ <view class="modal-content">
|
|
|
|
|
+ <view class="modal-row">
|
|
|
|
|
+ <text class="modal-label">用户</text>
|
|
|
|
|
+ <text class="modal-value">{{ lockUserInfo?.mobilePhone || '-' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="modal-row">
|
|
|
|
|
+ <text class="modal-label">锁定备注(仅平台人员可见)</text>
|
|
|
|
|
+ <input class="modal-input" type="text" v-model="lockRemark" placeholder="请输入锁定原因" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="modal-row">
|
|
|
|
|
+ <text class="modal-label">提示内容(用户启动设备时弹出)</text>
|
|
|
|
|
+ <input class="modal-input" type="text" v-model="lockNotice" placeholder="请输入要展示给用户的提示" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="modal-footer">
|
|
|
|
|
+ <button class="modal-btn cancel" @click="closeLockModal">取消</button>
|
|
|
|
|
+ <button class="modal-btn confirm" @click="confirmLock" :disabled="!lockNotice.trim()">确定锁定</button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 锁定记录弹窗 -->
|
|
|
|
|
+ <view class="modal-overlay" v-if="showLockRecordModal" @click="closeLockRecordModal">
|
|
|
|
|
+ <view class="modal-card lock-record-card" @click.stop>
|
|
|
|
|
+ <text class="modal-title">锁定记录</text>
|
|
|
|
|
+ <view class="modal-content">
|
|
|
|
|
+ <view class="modal-row">
|
|
|
|
|
+ <text class="modal-label">用户</text>
|
|
|
|
|
+ <text class="modal-value">{{ lockRecordUser?.mobilePhone || '-' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <scroll-view scroll-y class="lock-record-list" v-if="lockRecordList.length > 0">
|
|
|
|
|
+ <view class="lock-record-item" v-for="(rec, idx) in lockRecordList" :key="rec.id || idx">
|
|
|
|
|
+ <view class="lock-record-head">
|
|
|
|
|
+ <text class="lock-record-index">第 {{ lockRecordList.length - idx }} 次锁定</text>
|
|
|
|
|
+ <text class="lock-record-status" :class="rec.status === 1 ? 'locked' : 'unlocked'">
|
|
|
|
|
+ {{ rec.status === 1 ? '锁定中' : '已解锁' }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="lock-record-row">
|
|
|
|
|
+ <text class="lock-record-label">锁定时间</text>
|
|
|
|
|
+ <text class="lock-record-value">{{ formatTime(rec.lockTime) }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="lock-record-row">
|
|
|
|
|
+ <text class="lock-record-label">锁定人</text>
|
|
|
|
|
+ <text class="lock-record-value">{{ rec.lockOperator || '-' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="lock-record-row">
|
|
|
|
|
+ <text class="lock-record-label">锁定备注</text>
|
|
|
|
|
+ <text class="lock-record-value">{{ rec.lockRemark || '-' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="lock-record-row">
|
|
|
|
|
+ <text class="lock-record-label">提示内容</text>
|
|
|
|
|
+ <text class="lock-record-value">{{ rec.lockNotice || '-' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="lock-record-row" v-if="rec.unlockTime">
|
|
|
|
|
+ <text class="lock-record-label">解锁信息</text>
|
|
|
|
|
+ <text class="lock-record-value">{{ formatTime(rec.unlockTime) }} / {{ rec.unlockOperator || '-' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </scroll-view>
|
|
|
|
|
+ <view class="lock-record-empty" v-else-if="!lockRecordLoading">暂无锁定记录</view>
|
|
|
|
|
+ <view class="lock-record-empty" v-else>加载中...</view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="modal-footer">
|
|
|
|
|
+ <button class="modal-btn cancel" @click="closeLockRecordModal">关 闭</button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
</view>
|
|
</view>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
import { ref, onMounted } from 'vue'
|
|
import { ref, onMounted } from 'vue'
|
|
|
import { onReachBottom } from '@dcloudio/uni-app'
|
|
import { onReachBottom } from '@dcloudio/uni-app'
|
|
|
-import { getAppUserList } from '../../api/user.js'
|
|
|
|
|
|
|
+import { getAppUserList, lockUser, unlockUser, getUserLockRecords } from '../../api/user.js'
|
|
|
import { applyRefund } from '../../api/finance.js'
|
|
import { applyRefund } from '../../api/finance.js'
|
|
|
import { formatTime, showToast, formatAmount, fmtDictName, getDictColor, copyText } from '../../utils/index.js'
|
|
import { formatTime, showToast, formatAmount, fmtDictName, getDictColor, copyText } from '../../utils/index.js'
|
|
|
import { loadDicts } from '../../utils/dict.js'
|
|
import { loadDicts } from '../../utils/dict.js'
|
|
@@ -143,6 +237,14 @@ const searchKeyword = ref('')
|
|
|
const showRefundModal = ref(false)
|
|
const showRefundModal = ref(false)
|
|
|
const refundUser = ref(null)
|
|
const refundUser = ref(null)
|
|
|
const refundReason = ref('')
|
|
const refundReason = ref('')
|
|
|
|
|
+const showLockModal = ref(false)
|
|
|
|
|
+const lockUserInfo = ref(null)
|
|
|
|
|
+const lockRemark = ref('')
|
|
|
|
|
+const lockNotice = ref('')
|
|
|
|
|
+const showLockRecordModal = ref(false)
|
|
|
|
|
+const lockRecordUser = ref(null)
|
|
|
|
|
+const lockRecordList = ref([])
|
|
|
|
|
+const lockRecordLoading = ref(false)
|
|
|
|
|
|
|
|
const getStatusStyle = (status) => {
|
|
const getStatusStyle = (status) => {
|
|
|
const color = getDictColor('User.status', status)
|
|
const color = getDictColor('User.status', status)
|
|
@@ -252,6 +354,81 @@ const confirmRefund = async () => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const openLockModal = (item) => {
|
|
|
|
|
+ lockUserInfo.value = item
|
|
|
|
|
+ lockRemark.value = ''
|
|
|
|
|
+ lockNotice.value = ''
|
|
|
|
|
+ showLockModal.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const closeLockModal = () => {
|
|
|
|
|
+ showLockModal.value = false
|
|
|
|
|
+ lockUserInfo.value = null
|
|
|
|
|
+ lockRemark.value = ''
|
|
|
|
|
+ lockNotice.value = ''
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const confirmLock = async () => {
|
|
|
|
|
+ if (!lockNotice.value.trim()) return
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await lockUser({
|
|
|
|
|
+ userId: lockUserInfo.value.userId,
|
|
|
|
|
+ lockRemark: lockRemark.value.trim(),
|
|
|
|
|
+ lockNotice: lockNotice.value.trim()
|
|
|
|
|
+ })
|
|
|
|
|
+ if (res && res.code === 200) {
|
|
|
|
|
+ showToast('锁定成功')
|
|
|
|
|
+ closeLockModal()
|
|
|
|
|
+ loadData()
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('锁定用户失败:', error)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const openLockRecordModal = async (item) => {
|
|
|
|
|
+ lockRecordUser.value = item
|
|
|
|
|
+ lockRecordList.value = []
|
|
|
|
|
+ showLockRecordModal.value = true
|
|
|
|
|
+ lockRecordLoading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getUserLockRecords(item.userId)
|
|
|
|
|
+ if (res && res.code === 200) {
|
|
|
|
|
+ lockRecordList.value = res.data || []
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('加载锁定记录失败:', error)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ lockRecordLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const closeLockRecordModal = () => {
|
|
|
|
|
+ showLockRecordModal.value = false
|
|
|
|
|
+ lockRecordUser.value = null
|
|
|
|
|
+ lockRecordList.value = []
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleUnlock = (item) => {
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '解锁确认',
|
|
|
|
|
+ content: `确认解锁用户『${item.mobilePhone || '-'}』?解锁后该用户可正常启动设备与退款`,
|
|
|
|
|
+ success: async (modalRes) => {
|
|
|
|
|
+ if (!modalRes.confirm) return
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await unlockUser({ userId: item.userId })
|
|
|
|
|
+ if (res && res.code === 200) {
|
|
|
|
|
+ showToast('解锁成功')
|
|
|
|
|
+ loadData()
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('解锁用户失败:', error)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
onMounted(async () => {
|
|
onMounted(async () => {
|
|
|
await loadDicts()
|
|
await loadDicts()
|
|
|
loadData()
|
|
loadData()
|
|
@@ -461,6 +638,125 @@ onMounted(async () => {
|
|
|
line-height: 1.4;
|
|
line-height: 1.4;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.locked-tag {
|
|
|
|
|
+ color: #FFFFFF;
|
|
|
|
|
+ background-color: #C6171E;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lock-btn {
|
|
|
|
|
+ padding: 6rpx 20rpx;
|
|
|
|
|
+ background: #FFFFFF;
|
|
|
|
|
+ color: #C6171E;
|
|
|
|
|
+ border: 2rpx solid #C6171E;
|
|
|
|
|
+ border-radius: 16rpx;
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lock-btn:active {
|
|
|
|
|
+ background: #FFF0F0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.unlock-btn {
|
|
|
|
|
+ padding: 6rpx 20rpx;
|
|
|
|
|
+ background: #FFFFFF;
|
|
|
|
|
+ color: #31A24C;
|
|
|
|
|
+ border: 2rpx solid #31A24C;
|
|
|
|
|
+ border-radius: 16rpx;
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.unlock-btn:active {
|
|
|
|
|
+ background: #F0FFF4;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lock-info {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
+ gap: 12rpx;
|
|
|
|
|
+ margin-top: 12rpx;
|
|
|
|
|
+ padding-top: 12rpx;
|
|
|
|
|
+ border-top: 1rpx dashed #F0F0F0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lock-count-link {
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #C6171E;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 锁定记录弹窗 */
|
|
|
|
|
+.lock-record-card {
|
|
|
|
|
+ width: 660rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lock-record-list {
|
|
|
|
|
+ max-height: 640rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lock-record-item {
|
|
|
|
|
+ border: 1rpx solid #F0F0F0;
|
|
|
|
|
+ border-radius: 16rpx;
|
|
|
|
|
+ padding: 20rpx;
|
|
|
|
|
+ margin-bottom: 16rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lock-record-head {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lock-record-index {
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1A1A1A;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lock-record-status {
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ padding: 4rpx 16rpx;
|
|
|
|
|
+ border-radius: 20rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lock-record-status.locked {
|
|
|
|
|
+ color: #FFFFFF;
|
|
|
|
|
+ background-color: #C6171E;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lock-record-status.unlocked {
|
|
|
|
|
+ color: #666666;
|
|
|
|
|
+ background-color: #F0F0F0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lock-record-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ margin-bottom: 8rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lock-record-label {
|
|
|
|
|
+ width: 130rpx;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #999999;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lock-record-value {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #1A1A1A;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.lock-record-empty {
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ color: #999999;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 40rpx 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.load-more {
|
|
.load-more {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
justify-content: center;
|
|
justify-content: center;
|