feat: 调整余额支持用户搜索选择(昵称/openid前缀,不再需要手打28位完整openid)+ API前缀匹配
This commit is contained in:
@@ -102,9 +102,21 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 调整余额 -->
|
||||
<el-dialog v-model="adjustDlg" title="调整用户余额" width="400px">
|
||||
<el-dialog v-model="adjustDlg" title="调整用户余额" width="440px">
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="OpenID"><el-input v-model="adjustForm.openid" placeholder="用户openid" /></el-form-item>
|
||||
<el-form-item label="搜索用户">
|
||||
<el-input v-model="adjustForm.search" placeholder="输入昵称或openid前8位" clearable
|
||||
@input="searchUsers" style="width:100%" />
|
||||
<div v-if="searchResults.length" class="search-results">
|
||||
<div v-for="u in searchResults" :key="u._id" class="search-item" @click="pickUser(u)">
|
||||
<span>{{ u.nickname || '未命名' }}</span>
|
||||
<small>{{ (u.openid||'').slice(0,14) }}… | 星尘 {{ u.vpayCoins || 0 }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="pickedUser" class="picked-user">
|
||||
✓ {{ pickedUser.nickname || '未命名' }} | 星尘 {{ pickedUser.vpayCoins || 0 }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="变动值">
|
||||
<el-input-number v-model="adjustForm.amount" :min="-100000" :max="100000" />
|
||||
<span style="margin-left:8px;font-size:12px;color:#999">正=赠送 负=扣减</span>
|
||||
@@ -113,7 +125,7 @@
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="adjustDlg = false">取消</el-button>
|
||||
<el-button type="primary" @click="doAdjust">确认</el-button>
|
||||
<el-button type="primary" :disabled="!pickedUser" @click="doAdjust">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -134,7 +146,28 @@ const refundCoins = computed(() => {
|
||||
if (!o) return 0
|
||||
return o.metadata?.coins || Math.round((o.amount || 0) * 100)
|
||||
})
|
||||
const adjustDlg = ref(false), adjustForm = ref({ openid: '', amount: 100, reason: '' })
|
||||
const adjustDlg = ref(false), adjustForm = ref({ search: '', amount: 100, reason: '' })
|
||||
const searchResults = ref([]), pickedUser = ref(null)
|
||||
let searchTimer = null
|
||||
|
||||
function searchUsers() {
|
||||
if (searchTimer) clearTimeout(searchTimer)
|
||||
pickedUser.value = null
|
||||
searchResults.value = []
|
||||
const kw = adjustForm.value.search?.trim()
|
||||
if (!kw || kw.length < 2) return
|
||||
searchTimer = setTimeout(async () => {
|
||||
try {
|
||||
const r = await apiGet(`/api/admin/users?search=${encodeURIComponent(kw)}&limit=5`)
|
||||
searchResults.value = (r.items || []).slice(0, 5)
|
||||
} catch (e) {}
|
||||
}, 300)
|
||||
}
|
||||
function pickUser(u) {
|
||||
pickedUser.value = u
|
||||
searchResults.value = []
|
||||
adjustForm.value.search = u.nickname || (u.openid||'').slice(0,12)
|
||||
}
|
||||
|
||||
function fmt(t) { return t ? new Date(t).toLocaleString('zh-CN', { hour12: false }) : '—' }
|
||||
|
||||
@@ -173,10 +206,17 @@ async function doRefund() {
|
||||
}
|
||||
}
|
||||
async function doAdjust() {
|
||||
if (!pickedUser.value) return ElMessage.warning('请先搜索并选择用户')
|
||||
try {
|
||||
await apiPost('/api/admin/vpay/adjust', adjustForm.value)
|
||||
ElMessage.success('调整成功')
|
||||
await apiPost('/api/admin/vpay/adjust', {
|
||||
openid: pickedUser.value.openid,
|
||||
amount: adjustForm.value.amount,
|
||||
reason: adjustForm.value.reason,
|
||||
})
|
||||
ElMessage.success(`调整成功:${pickedUser.value.nickname || '用户'} ${(adjustForm.value.amount > 0 ? '+' : '') + adjustForm.value.amount} 星尘`)
|
||||
adjustDlg.value = false
|
||||
pickedUser.value = null
|
||||
adjustForm.value = { search: '', amount: 100, reason: '' }
|
||||
loadLedger(); loadStats()
|
||||
} catch (e) { ElMessage.error(e.message || '调整失败') }
|
||||
}
|
||||
@@ -192,3 +232,22 @@ onMounted(() => { loadStats(); load(); loadLedger() })
|
||||
.scard i { font-size: 11px; color: #a6abbb; font-style: normal; }
|
||||
.toolbar { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.search-results {
|
||||
position: absolute; top: 100%; left: 0; right: 0; z-index: 10;
|
||||
background: #fff; border: 1px solid #e4e7ed; border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,.1); max-height: 200px; overflow-y: auto;
|
||||
}
|
||||
.search-item {
|
||||
padding: 8px 12px; cursor: pointer; display: flex; flex-direction: column; gap: 2px;
|
||||
border-bottom: 1px solid #f5f7fa;
|
||||
}
|
||||
.search-item:hover { background: #f5f7fa; }
|
||||
.search-item small { color: #999; font-size: 11px; }
|
||||
.picked-user {
|
||||
margin-top: 6px; padding: 6px 10px; background: #f0f9eb;
|
||||
border: 1px solid #e1f3d8; border-radius: 6px; font-size: 13px; color: #67c23a;
|
||||
}
|
||||
.el-form-item { position: relative; }
|
||||
</style>
|
||||
|
||||
@@ -1339,8 +1339,14 @@ router.post('/vpay/adjust', requirePermission('orders:write'), async (req, res)
|
||||
try {
|
||||
const { openid, amount, reason } = req.body
|
||||
if (!openid || !amount || Math.abs(amount) > 100000) return ApiResponse.error(res, '参数无效', 400)
|
||||
const user = await User.findOne({ openid })
|
||||
if (!user) return ApiResponse.notFound(res, '用户不存在')
|
||||
// 支持前缀匹配:输入至少8位openid前缀即可(完整openid有28位,手打太长)
|
||||
let user = await User.findOne({ openid })
|
||||
if (!user && openid.length >= 8) {
|
||||
const matches = await User.find({ openid: { $regex: '^' + openid } }).limit(5)
|
||||
if (matches.length === 1) user = matches[0]
|
||||
else if (matches.length > 1) return ApiResponse.error(res, `匹配到 ${matches.length} 个用户,请输入更多字符`, 400)
|
||||
}
|
||||
if (!user) return ApiResponse.notFound(res, '用户不存在(支持输入openid前8位以上)')
|
||||
|
||||
user.vpayCoins = (user.vpayCoins || 0) + amount
|
||||
await user.save()
|
||||
|
||||
Reference in New Issue
Block a user