This commit is contained in:
parent
319c46f29c
commit
5a04e003ca
|
|
@ -11,7 +11,9 @@
|
|||
|
||||
<el-table :data="list" v-loading="loading" stripe :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }" style="width: 100%;">
|
||||
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||||
<el-table-column prop="userId" label="用户ID" width="90" align="center" />
|
||||
<el-table-column label="UID" width="100" align="center">
|
||||
<template #default="{ row }">{{ row.userUid || row.userId }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="userNickname" label="用户昵称" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column prop="realName" label="真实姓名" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="phone" label="手机号" min-width="140" />
|
||||
|
|
@ -47,7 +49,12 @@ const statusFilter = ref('')
|
|||
|
||||
const statusLabel = (s) => ({ Pending: '待审核', Approved: '已通过', Rejected: '已拒绝' }[s] || s)
|
||||
const statusTagType = (s) => ({ Pending: 'warning', Approved: 'success', Rejected: 'danger' }[s] || 'info')
|
||||
const formatTime = (t) => t ? new Date(t).toLocaleString('zh-CN') : ''
|
||||
const formatTime = (t) => {
|
||||
if (!t) return ''
|
||||
const d = new Date(typeof t === 'string' && !t.endsWith('Z') ? t + 'Z' : t)
|
||||
const pad = n => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
|
|
|
|||
|
|
@ -25,23 +25,23 @@
|
|||
<el-tag :type="statusTagType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单主" min-width="120">
|
||||
<el-table-column label="单主" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; align-items: center; gap: 6px;">
|
||||
<el-avatar :size="28" :src="row.ownerAvatar || undefined">
|
||||
<span style="font-size: 12px">{{ (row.ownerNickname || '?')[0] }}</span>
|
||||
</el-avatar>
|
||||
<span>{{ row.ownerNickname }}</span>
|
||||
<span>{{ row.ownerNickname }} <span style="color:#999;font-size:11px;">{{ row.ownerUid }}</span></span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="跑腿" min-width="120">
|
||||
<el-table-column label="跑腿" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; align-items: center; gap: 6px;">
|
||||
<el-avatar :size="28" :src="row.runnerAvatar || undefined">
|
||||
<span style="font-size: 12px">{{ (row.runnerNickname || '?')[0] }}</span>
|
||||
</el-avatar>
|
||||
<span>{{ row.runnerNickname }}</span>
|
||||
<span>{{ row.runnerNickname }} <span style="color:#999;font-size:11px;">{{ row.runnerUid }}</span></span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
|
@ -109,7 +109,12 @@ const previewUrl = ref('')
|
|||
const typeLabel = (t) => ({ Pickup: '代取', Delivery: '代送', Help: '万能帮', Purchase: '代购', Food: '美食街' }[t] || t)
|
||||
const statusLabel = (s) => ({ Pending: '待接单', InProgress: '进行中', WaitConfirm: '待确认', Completed: '已完成', Cancelled: '已取消', Appealing: '申诉中' }[s] || s)
|
||||
const statusTagType = (s) => ({ Pending: 'info', InProgress: 'primary', WaitConfirm: 'warning', Completed: 'success', Cancelled: 'danger', Appealing: '' }[s] || 'info')
|
||||
const formatTime = (t) => t ? new Date(t).toLocaleString('zh-CN') : ''
|
||||
const formatTime = (t) => {
|
||||
if (!t) return ''
|
||||
const d = new Date(typeof t === 'string' && !t.endsWith('Z') ? t + 'Z' : t)
|
||||
const pad = n => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
const filteredList = computed(() => {
|
||||
let data = list.value
|
||||
|
|
@ -178,7 +183,7 @@ function parseIMMessages(imResponse, orderInfo) {
|
|||
const showTime = timestamp - lastTime > 300000
|
||||
let timeLabel = ''
|
||||
if (showTime && timestamp) {
|
||||
timeLabel = new Date(timestamp).toLocaleString('zh-CN')
|
||||
timeLabel = formatTime(timestamp / 1000 > 1e12 ? timestamp : timestamp * 1000)
|
||||
lastTime = timestamp
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -140,6 +140,22 @@
|
|||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 最低提现 -->
|
||||
<el-tab-pane label="最低提现" name="min_withdrawal">
|
||||
<el-form label-width="120px" style="max-width: 500px;">
|
||||
<el-form-item label="最低提现">
|
||||
<el-input-number v-model="minWithdrawal" :min="0.01" :max="1000" :step="1" :precision="2" />
|
||||
<span style="margin-left: 8px; color: #909399;">元</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<span style="color: #909399; font-size: 13px;">小程序提现页的最低提现金额,修改后提示文字自动更新</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="saveMinWithdrawal">保存</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 冻结时间 -->
|
||||
<el-tab-pane label="冻结时间" name="freeze_days">
|
||||
<el-form label-width="120px" style="max-width: 500px;">
|
||||
|
|
@ -201,6 +217,7 @@ const commissionRules = ref([])
|
|||
const configs = reactive({ qrcode: '', agreement: '', privacy: '', runner_agreement: '', withdrawal_guide: '' })
|
||||
const freezeDays = ref(1)
|
||||
const minCommission = ref(1.0)
|
||||
const minWithdrawal = ref(1.0)
|
||||
|
||||
// 页面顶图配置
|
||||
const pageBannerList = [
|
||||
|
|
@ -260,7 +277,7 @@ async function saveConfig(key) {
|
|||
async function fetchFreezeDays() {
|
||||
try {
|
||||
const res = await request.get('/admin/config/freeze_days')
|
||||
freezeDays.value = parseInt(res.value) || 1
|
||||
freezeDays.value = res?.value !== undefined && res.value !== '' ? parseInt(res.value) : 1
|
||||
} catch { /* 忽略 */ }
|
||||
}
|
||||
|
||||
|
|
@ -291,6 +308,23 @@ async function saveMinCommission() {
|
|||
}
|
||||
}
|
||||
|
||||
async function fetchMinWithdrawal() {
|
||||
try {
|
||||
const res = await request.get('/admin/config/min_withdrawal')
|
||||
if (res?.value) minWithdrawal.value = parseFloat(res.value) || 1.0
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function saveMinWithdrawal() {
|
||||
saving.value = true
|
||||
try {
|
||||
await request.put('/admin/config/min_withdrawal', { value: String(minWithdrawal.value) })
|
||||
ElMessage.success('最低提现金额保存成功')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPageBanners() {
|
||||
for (const item of pageBannerList) {
|
||||
try {
|
||||
|
|
@ -322,6 +356,7 @@ onMounted(async () => {
|
|||
fetchConfig('withdrawal_guide'),
|
||||
fetchFreezeDays(),
|
||||
fetchMinCommission(),
|
||||
fetchMinWithdrawal(),
|
||||
fetchPageBanners()
|
||||
])
|
||||
})
|
||||
|
|
|
|||
|
|
@ -98,7 +98,12 @@ const rules = {
|
|||
|
||||
const targetLabel = (t) => ({ All: '全部', OrderUser: '下单用户', RunnerUser: '跑腿用户', Specific: '指定用户' }[t] || t)
|
||||
const targetTagType = (t) => ({ All: '', OrderUser: 'success', RunnerUser: 'warning', Specific: 'info' }[t] || '')
|
||||
const formatTime = (t) => t ? new Date(t).toLocaleString('zh-CN') : ''
|
||||
const formatTime = (t) => {
|
||||
if (!t) return ''
|
||||
const d = new Date(typeof t === 'string' && !t.endsWith('Z') ? t + 'Z' : t)
|
||||
const pad = n => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
const valid = await formRef.value.validate().catch(() => false)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
</div>
|
||||
|
||||
<el-table :data="list" v-loading="loading" border>
|
||||
<el-table-column prop="orderNo" label="订单编号" width="160" />
|
||||
<el-table-column prop="orderNo" label="订单编号" width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="orderType" label="类型" width="80">
|
||||
<template #default="{ row }">{{ typeLabel(row.orderType) }}</template>
|
||||
</el-table-column>
|
||||
|
|
@ -31,10 +31,25 @@
|
|||
<el-tag :type="statusTagType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="itemName" label="内容" show-overflow-tooltip />
|
||||
<el-table-column prop="commission" label="佣金" width="80" />
|
||||
<el-table-column prop="totalAmount" label="总额" width="80" />
|
||||
<el-table-column prop="createdAt" label="下单时间" width="170">
|
||||
<el-table-column label="单主" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.ownerNickname || '-' }}
|
||||
<span style="color:#999;font-size:11px;">{{ row.ownerUid || row.ownerId }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="跑腿" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.runnerId">
|
||||
{{ row.runnerNickname || '-' }}
|
||||
<span style="color:#999;font-size:11px;">{{ row.runnerUid || row.runnerId }}</span>
|
||||
</template>
|
||||
<span v-else style="color:#ccc;">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="itemName" label="内容" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column prop="commission" label="佣金" width="70" />
|
||||
<el-table-column prop="totalAmount" label="总额" width="70" />
|
||||
<el-table-column label="下单时间" width="160">
|
||||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="240" fixed="right">
|
||||
|
|
@ -117,7 +132,12 @@ const cancelReason = ref('')
|
|||
const typeLabel = (t) => ({ Pickup: '代取', Delivery: '代送', Help: '万能帮', Purchase: '代购', Food: '美食街' }[t] || t)
|
||||
const statusLabel = (s) => ({ Pending: '待接单', InProgress: '进行中', WaitConfirm: '待确认', Completed: '已完成', Cancelled: '已取消', Appealing: '申诉中' }[s] || s)
|
||||
const statusTagType = (s) => ({ Pending: 'info', InProgress: 'primary', WaitConfirm: 'warning', Completed: 'success', Cancelled: 'danger', Appealing: '' }[s] || 'info')
|
||||
const formatTime = (t) => t ? new Date(t).toLocaleString('zh-CN') : ''
|
||||
const formatTime = (t) => {
|
||||
if (!t) return ''
|
||||
const d = new Date(typeof t === 'string' && !t.endsWith('Z') ? t + 'Z' : t)
|
||||
const pad = n => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
|
|
|
|||
|
|
@ -148,7 +148,8 @@ function getScoreColor(score) {
|
|||
|
||||
function formatTime(str) {
|
||||
if (!str) return '-'
|
||||
const d = new Date(str)
|
||||
// 确保 UTC 时间正确解析
|
||||
const d = new Date(typeof str === 'string' && !str.endsWith('Z') ? str + 'Z' : str)
|
||||
const pad = n => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@
|
|||
</div>
|
||||
|
||||
<el-table :data="filteredList" v-loading="loading" stripe style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column label="UID" width="100" align="center">
|
||||
<template #default="{ row }">{{ row.uid || row.id }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="nickname" label="昵称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="phone" label="手机号" min-width="130" />
|
||||
<el-table-column label="评分" width="160" align="center">
|
||||
|
|
@ -123,7 +125,8 @@ const searchUid = ref('')
|
|||
|
||||
const filteredList = computed(() => {
|
||||
if (!searchUid.value) return list.value
|
||||
return list.value.filter(r => String(r.id).includes(searchUid.value.trim()))
|
||||
const kw = searchUid.value.trim()
|
||||
return list.value.filter(r => String(r.uid || r.id).includes(kw) || String(r.id).includes(kw))
|
||||
})
|
||||
const reviewDialogVisible = ref(false)
|
||||
const reviewLoading = ref(false)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@
|
|||
</div>
|
||||
|
||||
<el-table :data="list" v-loading="loading" stripe style="width: 100%">
|
||||
<el-table-column prop="id" label="UID" width="80" align="center" />
|
||||
<el-table-column label="UID" width="100" align="center">
|
||||
<template #default="{ row }">{{ row.uid || row.id }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="头像" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-avatar :size="36" :src="row.avatarUrl || undefined">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name" : "校园跑腿侠",
|
||||
"name" : "校跑侠校园服务",
|
||||
"appid" : "__UNI__F1854F8",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.0",
|
||||
|
|
|
|||
|
|
@ -94,6 +94,8 @@ export default {
|
|||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
line-height: 1.8;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@
|
|||
<!-- 提交按钮 -->
|
||||
<view class="submit-section">
|
||||
<button class="submit-btn" @click="onSubmit" :loading="submitting" :disabled="submitting">
|
||||
支付佣金 确定下单
|
||||
确定下单
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
|
|
|||
|
|
@ -442,6 +442,10 @@ export default {
|
|||
border: none;
|
||||
}
|
||||
|
||||
.modal-btn.confirm::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.modal-btn.confirm {
|
||||
background-color: #FAD146;
|
||||
color: #333333;
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ export default {
|
|||
const sysInfo = uni.getSystemInfoSync()
|
||||
this.statusBarHeight = sysInfo.statusBarHeight || 0
|
||||
this.loadMinCommission()
|
||||
this.restoreFormData()
|
||||
// 如果购物车为空,返回上一页
|
||||
if (this.cartStore.totalCount === 0) {
|
||||
uni.showToast({ title: '购物车为空', icon: 'none' })
|
||||
|
|
@ -115,6 +116,17 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
goBack() { uni.navigateBack() },
|
||||
/** 恢复登录前保存的表单数据 */
|
||||
restoreFormData() {
|
||||
const saved = uni.getStorageSync('loginFormData')
|
||||
if (saved) {
|
||||
try {
|
||||
const data = JSON.parse(saved)
|
||||
Object.assign(this.form, data)
|
||||
} catch (e) {}
|
||||
uni.removeStorageSync('loginFormData')
|
||||
}
|
||||
},
|
||||
async loadMinCommission() {
|
||||
try {
|
||||
const res = await getMinCommission()
|
||||
|
|
@ -164,6 +176,9 @@ export default {
|
|||
// 未登录跳转登录页
|
||||
const token = uni.getStorageSync('token')
|
||||
if (!token) {
|
||||
// 保存表单数据,登录后恢复
|
||||
uni.setStorageSync('loginFormData', JSON.stringify(this.form))
|
||||
uni.setStorageSync('loginRedirect', '/pages/food/food-order')
|
||||
uni.navigateTo({ url: '/pages/login/login' })
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@
|
|||
<view class="submit-section">
|
||||
<text class="pay-amount">支付金额:¥{{ payAmount }}</text>
|
||||
<button class="submit-btn" @click="onSubmit" :loading="submitting" :disabled="submitting">
|
||||
支付佣金 确定下单
|
||||
确定下单
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -96,7 +96,7 @@
|
|||
payAmount() {
|
||||
const goods = parseFloat(this.form.goodsAmount) || 0
|
||||
const commission = parseFloat(this.form.commission) || 0
|
||||
return (goods + commission).toFixed(1)
|
||||
return (goods + commission).toFixed(2)
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
|
||||
<!-- 聊天记录区域 -->
|
||||
<scroll-view class="chat-body" scroll-y :scroll-top="scrollTop" scroll-with-animation
|
||||
@scrolltoupper="loadMoreHistory">
|
||||
@scrolltoupper="loadMoreHistory" :show-scrollbar="false">
|
||||
<!-- 聊天安全提示 -->
|
||||
<view class="chat-safety-tip">
|
||||
<text>聊天安全提示:请友好和谐沟通。请友好和谐沟通。请友好和谐沟通。请友好和谐沟通。</text>
|
||||
|
|
@ -118,7 +118,7 @@
|
|||
<image class="msg-avatar" :src="msg.avatar || '/static/logo.png'" mode="aspectFill"></image>
|
||||
<view class="msg-bubble">
|
||||
<image v-if="msg.type === 'image'" class="msg-image" :src="msg.content" mode="widthFix"
|
||||
@click="previewImage(msg.content)"></image>
|
||||
@click="previewImage(msg.originUrl || msg.content)"></image>
|
||||
<text v-else class="msg-text">{{ msg.content }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -164,7 +164,7 @@
|
|||
<view class="panel-icon-wrap"><image class="panel-icon-img" src="/static/ic_errand.png" mode="aspectFit"></image></view>
|
||||
<text class="panel-label">更改跑腿价格</text>
|
||||
</view>
|
||||
<view class="panel-item" v-if="isRunner" @click="onCompleteOrder">
|
||||
<view class="panel-item" v-if="isRunner && orderInfo.status === 'InProgress'" @click="onCompleteOrder">
|
||||
<view class="panel-icon-wrap"><image class="panel-icon-img" src="/static/ic_complete.png" mode="aspectFit"></image></view>
|
||||
<text class="panel-label">完成订单</text>
|
||||
</view>
|
||||
|
|
@ -219,7 +219,8 @@
|
|||
sendTextMessage,
|
||||
sendImageMessage,
|
||||
sendCustomMessage,
|
||||
formatIMMessage
|
||||
formatIMMessage,
|
||||
setMessageRead
|
||||
} from '../../utils/im'
|
||||
|
||||
export default {
|
||||
|
|
@ -359,8 +360,12 @@
|
|||
}
|
||||
}
|
||||
this.scrollToBottom()
|
||||
// 收到新消息时标记已读
|
||||
if (this.targetImUserId) setMessageRead(this.targetImUserId)
|
||||
})
|
||||
await this.loadHistory()
|
||||
// 标记会话已读
|
||||
if (this.targetImUserId) setMessageRead(this.targetImUserId)
|
||||
} catch (e) {
|
||||
console.error('[IM] 初始化失败:', e)
|
||||
uni.showToast({
|
||||
|
|
@ -562,14 +567,10 @@
|
|||
if (msg) msg.status = res.status
|
||||
const actionLabel = action === 'Accepted' ? '同意' : '拒绝'
|
||||
const changeTypeLabel = res.changeType === 'Commission' ? '跑腿佣金' : '商品总额'
|
||||
this.chatMessages.push({
|
||||
id: `sys_${Date.now()}`,
|
||||
type: 'system',
|
||||
content: `您已${actionLabel}${changeTypeLabel}改价`
|
||||
})
|
||||
|
||||
// 通过 IM 通知对方改价结果
|
||||
// 通过 IM 通知对方改价结果(对方收到后显示为系统提示)
|
||||
if (this.imReady) {
|
||||
// 发给对方
|
||||
await sendCustomMessage(this.targetImUserId, {
|
||||
bizType: 'price-change-response',
|
||||
priceChangeId: priceChangeId,
|
||||
|
|
@ -578,6 +579,17 @@
|
|||
changeTypeLabel,
|
||||
description: `${actionLabel}了${changeTypeLabel}改价申请`
|
||||
}, this.orderId)
|
||||
// 发一条文本消息记录操作(双方都能看到,持久化)
|
||||
try {
|
||||
const imMsg = await sendTextMessage(this.targetImUserId, `[系统提示] 单主已${actionLabel}${changeTypeLabel}改价`, this.orderId)
|
||||
this.chatMessages.push(formatIMMessage(imMsg, this.imUserId))
|
||||
} catch (ex) {}
|
||||
} else {
|
||||
this.chatMessages.push({
|
||||
id: `sys_${Date.now()}`,
|
||||
type: 'system',
|
||||
content: `您已${actionLabel}${changeTypeLabel}改价`
|
||||
})
|
||||
}
|
||||
|
||||
if (action === 'Accepted') {
|
||||
|
|
@ -585,11 +597,13 @@
|
|||
if (res.paymentParams) {
|
||||
try {
|
||||
await this.wxPay(res.paymentParams)
|
||||
this.chatMessages.push({
|
||||
id: `sys_pay_${Date.now()}`,
|
||||
type: 'system',
|
||||
content: `已补缴¥${res.difference.toFixed(2)}`
|
||||
})
|
||||
// 补缴成功,通过IM通知双方
|
||||
if (this.imReady && this.targetImUserId) {
|
||||
try {
|
||||
const imMsg = await sendTextMessage(this.targetImUserId, '[系统提示] 单主已完成补缴支付', this.orderId)
|
||||
this.chatMessages.push(formatIMMessage(imMsg, this.imUserId))
|
||||
} catch (ex) {}
|
||||
}
|
||||
} catch (e) {
|
||||
// 支付取消或失败,保存待支付信息供再次支付
|
||||
this.pendingPayment = res.paymentParams
|
||||
|
|
@ -602,11 +616,19 @@
|
|||
}
|
||||
// 需要退款:后端已自动退款
|
||||
if (res.difference < 0) {
|
||||
this.chatMessages.push({
|
||||
id: `sys_r_${Date.now()}`,
|
||||
type: 'system',
|
||||
content: res.refundSuccess ? `已退还您¥${Math.abs(res.difference).toFixed(2)}` : '退款处理中,请稍后查看'
|
||||
})
|
||||
const refundText = res.refundSuccess
|
||||
? `改价退款¥${Math.abs(res.difference).toFixed(2)}已原路返回`
|
||||
: '退款处理中,请稍后查看'
|
||||
// 通过IM发送(持久化,双方可见)
|
||||
if (this.imReady && this.targetImUserId) {
|
||||
try {
|
||||
await sendCustomMessage(this.targetImUserId, {
|
||||
bizType: 'order-status',
|
||||
description: refundText
|
||||
}, this.orderId)
|
||||
} catch (ex) {}
|
||||
}
|
||||
this.chatMessages.push({ id: `sys_r_${Date.now()}`, type: 'system', content: refundText })
|
||||
}
|
||||
await this.loadOrderInfo()
|
||||
}
|
||||
|
|
@ -636,7 +658,7 @@
|
|||
|
||||
const statusMessages = {
|
||||
'InProgress→WaitConfirm': '跑腿已提交完成,请在订单详情中确认',
|
||||
'WaitConfirm→Completed': '单主已确认,订单已完成',
|
||||
'WaitConfirm→Completed': '订单已完成',
|
||||
'WaitConfirm→InProgress': '单主已拒绝完成,订单继续进行'
|
||||
}
|
||||
const key = `${oldStatus}→${newStatus}`
|
||||
|
|
@ -647,6 +669,16 @@
|
|||
const lastMsg = [...this.chatMessages].reverse().find(m => m.type === 'system')
|
||||
if (lastMsg && lastMsg.content === msg) return
|
||||
|
||||
// 通过 IM 发送状态变化消息(持久化,双方可见)
|
||||
if (this.imReady && this.targetImUserId) {
|
||||
try {
|
||||
await sendCustomMessage(this.targetImUserId, {
|
||||
bizType: 'order-status',
|
||||
action: key,
|
||||
description: msg
|
||||
}, this.orderId)
|
||||
} catch (e) {}
|
||||
}
|
||||
this.chatMessages.push({
|
||||
id: `sys_status_${Date.now()}`,
|
||||
type: 'system',
|
||||
|
|
@ -720,6 +752,15 @@
|
|||
try {
|
||||
await this.wxPay(this.pendingPayment)
|
||||
uni.showToast({ title: '补缴成功', icon: 'success' })
|
||||
// 发送补缴成功的提示消息
|
||||
if (this.imReady && this.targetImUserId) {
|
||||
try {
|
||||
const imMsg = await sendTextMessage(this.targetImUserId, '[系统提示] 单主已完成补缴支付', this.orderId)
|
||||
this.chatMessages.push(formatIMMessage(imMsg, this.imUserId))
|
||||
this.scrollToBottom()
|
||||
} catch (ex) {}
|
||||
}
|
||||
await this.loadOrderInfo()
|
||||
} catch (e) {}
|
||||
},
|
||||
previewImage(url) {
|
||||
|
|
@ -890,10 +931,18 @@
|
|||
/* 聊天记录 */
|
||||
.chat-body {
|
||||
flex: 1;
|
||||
width: 96%;
|
||||
padding: 16rpx 0;
|
||||
margin: 0 auto 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.chat-body ::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.loading-history {
|
||||
text-align: center;
|
||||
padding: 16rpx 0;
|
||||
|
|
@ -1289,4 +1338,13 @@
|
|||
background: #FAD146;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 隐藏聊天页滚动条(非scoped才能覆盖scroll-view内部) */
|
||||
.chat-body ::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -82,7 +82,7 @@
|
|||
class="form-input"
|
||||
v-model="withdrawForm.amount"
|
||||
type="digit"
|
||||
placeholder="最低1元,支持小数点两位"
|
||||
:placeholder="`最低${minWithdrawal}元,支持小数点两位`"
|
||||
/>
|
||||
</view>
|
||||
|
||||
|
|
@ -133,7 +133,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { getEarnings, getWithdrawals, applyWithdraw, getWithdrawalGuide } from '../../utils/api'
|
||||
import { getEarnings, getWithdrawals, applyWithdraw, getWithdrawalGuide, getMinWithdrawal } from '../../utils/api'
|
||||
import { uploadFile } from '../../utils/request'
|
||||
|
||||
export default {
|
||||
|
|
@ -158,16 +158,25 @@ export default {
|
|||
// 提现说明弹窗
|
||||
showGuideModal: false,
|
||||
guideContent: '',
|
||||
statusBarHeight: 0
|
||||
statusBarHeight: 0,
|
||||
minWithdrawal: 1
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
const sysInfo = uni.getSystemInfoSync()
|
||||
this.statusBarHeight = sysInfo.statusBarHeight || 0
|
||||
this.loadData()
|
||||
this.loadMinWithdrawal()
|
||||
},
|
||||
methods: {
|
||||
goBack() { uni.navigateBack() },
|
||||
/** 加载最低提现金额配置 */
|
||||
async loadMinWithdrawal() {
|
||||
try {
|
||||
const res = await getMinWithdrawal()
|
||||
if (res?.value) this.minWithdrawal = parseFloat(res.value) || 1
|
||||
} catch (e) {}
|
||||
},
|
||||
/** 加载收益和提现数据 */
|
||||
async loadData() {
|
||||
try {
|
||||
|
|
@ -233,8 +242,8 @@ export default {
|
|||
const amount = parseFloat(this.withdrawForm.amount)
|
||||
|
||||
// 金额校验
|
||||
if (isNaN(amount) || amount < 1) {
|
||||
uni.showToast({ title: '提现金额最低1元', icon: 'none' })
|
||||
if (isNaN(amount) || amount < this.minWithdrawal) {
|
||||
uni.showToast({ title: `提现金额最低${this.minWithdrawal}元`, icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -624,12 +633,7 @@ export default {
|
|||
border: none;
|
||||
}
|
||||
|
||||
.modal-btn.cancel {
|
||||
background-color: #f5f5f5;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.modal-btn.cancel::after {
|
||||
.modal-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@
|
|||
value: 'Food'
|
||||
}
|
||||
],
|
||||
currentTab: 'Pickup',
|
||||
currentTab: '',
|
||||
sortOptions: [{
|
||||
label: '按时间排序',
|
||||
value: 'time'
|
||||
|
|
@ -225,12 +225,12 @@
|
|||
certStatus: null
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
async onShow() {
|
||||
const sysInfo = uni.getSystemInfoSync()
|
||||
this.statusBarHeight = sysInfo.statusBarHeight || 0
|
||||
this.certStatus = null
|
||||
this.loadBanner()
|
||||
this.loadTabs()
|
||||
await this.loadTabs()
|
||||
this.loadOrders()
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -239,14 +239,16 @@
|
|||
try {
|
||||
const entries = await getServiceEntries()
|
||||
if (entries && entries.length > 0) {
|
||||
// 服务入口名称到订单类型的映射
|
||||
const nameMap = { '代取': 'Pickup', '代送': 'Delivery', '万能帮': 'Help', '代购': 'Purchase', '美食街': 'Food' }
|
||||
const sorted = entries
|
||||
.filter(e => nameMap[e.name])
|
||||
.map(e => ({ label: e.name, value: nameMap[e.name] }))
|
||||
if (sorted.length > 0) {
|
||||
this.tabs = sorted
|
||||
this.currentTab = sorted[0].value
|
||||
// 只在首次或当前tab不在列表中时重置
|
||||
if (!this.currentTab || !sorted.find(t => t.value === this.currentTab)) {
|
||||
this.currentTab = sorted[0].value
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
|
@ -753,6 +755,10 @@
|
|||
border: none;
|
||||
}
|
||||
|
||||
.modal-btn.confirm::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.modal-btn.confirm {
|
||||
background: linear-gradient(135deg, #FFB700, #FF9500);
|
||||
color: #fff;
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@
|
|||
import { getOrderDetail, completeOrder, confirmOrder, rejectOrder } from '../../utils/api'
|
||||
import { uploadFile } from '../../utils/request'
|
||||
import { useUserStore } from '../../stores/user'
|
||||
import { initIM, sendCustomMessage } from '../../utils/im'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
|
@ -165,6 +166,17 @@ export default {
|
|||
|
||||
await completeOrder(this.orderId, { completionProof })
|
||||
|
||||
// 通过 IM 通知单主
|
||||
try {
|
||||
await initIM()
|
||||
const targetImUserId = `user_${this.orderInfo.ownerId}`
|
||||
await sendCustomMessage(targetImUserId, {
|
||||
bizType: 'order-status',
|
||||
action: 'InProgress→WaitConfirm',
|
||||
description: '跑腿已提交完成,请在订单详情中确认'
|
||||
}, this.orderId)
|
||||
} catch (ex) {}
|
||||
|
||||
uni.showToast({ title: '已提交完成', icon: 'success' })
|
||||
|
||||
// 返回聊天页
|
||||
|
|
|
|||
|
|
@ -683,6 +683,10 @@ export default {
|
|||
border: none;
|
||||
}
|
||||
|
||||
.modal-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.modal-btn.cancel {
|
||||
background-color: #f5f5f5;
|
||||
color: #666666;
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@
|
|||
<!-- 提交按钮 -->
|
||||
<view class="submit-section">
|
||||
<button class="submit-btn" @click="onSubmit" :loading="submitting" :disabled="submitting">
|
||||
支付佣金 确定下单
|
||||
确定下单
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@
|
|||
<view class="submit-section">
|
||||
<text class="pay-amount">支付金额:¥{{ payAmount }}</text>
|
||||
<button class="submit-btn" @click="onSubmit" :loading="submitting" :disabled="submitting">
|
||||
支付佣金 确定下单
|
||||
确定下单
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -111,7 +111,7 @@
|
|||
payAmount() {
|
||||
const goods = parseFloat(this.form.goodsAmount) || 0
|
||||
const commission = parseFloat(this.form.commission) || 0
|
||||
return (goods + commission).toFixed(1)
|
||||
return (goods + commission).toFixed(2)
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
|
|
|
|||
|
|
@ -227,6 +227,11 @@ export function getMinCommission() {
|
|||
return request({ url: '/api/config/min-commission' })
|
||||
}
|
||||
|
||||
/** 获取最低提现金额配置 */
|
||||
export function getMinWithdrawal() {
|
||||
return request({ url: '/api/config/min-withdrawal' })
|
||||
}
|
||||
|
||||
// ==================== 腾讯 IM ====================
|
||||
|
||||
/** 获取 IM UserSig */
|
||||
|
|
|
|||
|
|
@ -109,6 +109,20 @@ export function getConversationId(targetUserId) {
|
|||
return `C2C${targetUserId}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记会话为已读
|
||||
* @param {string} targetUserId - 对方用户 ID
|
||||
*/
|
||||
export async function setMessageRead(targetUserId) {
|
||||
if (!chat || !isReady) return
|
||||
try {
|
||||
const conversationID = getConversationId(targetUserId)
|
||||
await chat.setMessageRead({ conversationID })
|
||||
} catch (e) {
|
||||
console.error('[IM] 标记已读失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表(用于消息页展示聊天记录)
|
||||
* @returns {Promise<Array>} 会话列表
|
||||
|
|
@ -261,11 +275,15 @@ export function formatIMMessage(msg, currentImUserId) {
|
|||
|
||||
// 图片消息
|
||||
if (msg.type === TencentCloudChat.TYPES.MSG_IMAGE) {
|
||||
const imageInfo = msg.payload.imageInfoArray?.[1] || msg.payload.imageInfoArray?.[0]
|
||||
const imgArr = msg.payload.imageInfoArray || []
|
||||
// [0]原图 [1]大图 [2]缩略图,显示用大图,预览用原图
|
||||
const displayUrl = imgArr[1]?.url || imgArr[0]?.url || ''
|
||||
const originUrl = imgArr[0]?.url || imgArr[1]?.url || ''
|
||||
return {
|
||||
id: msg.ID,
|
||||
type: 'image',
|
||||
content: imageInfo?.url || '',
|
||||
content: displayUrl,
|
||||
originUrl: originUrl,
|
||||
isSelf,
|
||||
avatar,
|
||||
time: msg.time * 1000,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
*/
|
||||
|
||||
// API 基础地址,按环境切换
|
||||
// const BASE_URL = 'http://localhost:5099'
|
||||
const BASE_URL = 'http://api.zwz.shhmkjgs.cn'
|
||||
const BASE_URL = 'http://localhost:5099'
|
||||
// const BASE_URL = 'http://api.zwz.shhmkjgs.cn'
|
||||
|
||||
/**
|
||||
* 获取本地存储的 token
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ public static class AdminEndpoints
|
|||
.Select(u => new
|
||||
{
|
||||
u.Id,
|
||||
u.Uid,
|
||||
u.Nickname,
|
||||
u.AvatarUrl,
|
||||
u.Phone,
|
||||
|
|
@ -106,7 +107,6 @@ public static class AdminEndpoints
|
|||
u.RunnerScore,
|
||||
u.IsBanned,
|
||||
u.CreatedAt,
|
||||
// 查询该用户的订单数
|
||||
OrderCount = db.Orders.Count(o => o.OwnerId == u.Id)
|
||||
})
|
||||
.ToListAsync();
|
||||
|
|
@ -198,6 +198,7 @@ public static class AdminEndpoints
|
|||
.Join(db.Users, c => c.UserId, u => u.Id, (c, u) => new
|
||||
{
|
||||
u.Id,
|
||||
u.Uid,
|
||||
u.Nickname,
|
||||
Phone = c.Phone,
|
||||
u.RunnerScore,
|
||||
|
|
@ -359,7 +360,10 @@ public static class AdminEndpoints
|
|||
// 管理端获取订单列表
|
||||
app.MapGet("/api/admin/orders", async (string? status, string? orderType, AppDbContext db) =>
|
||||
{
|
||||
var query = db.Orders.AsQueryable();
|
||||
var query = db.Orders
|
||||
.Include(o => o.Owner)
|
||||
.Include(o => o.Runner)
|
||||
.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(status) && Enum.TryParse<OrderStatus>(status, true, out var s))
|
||||
query = query.Where(o => o.Status == s);
|
||||
|
|
@ -374,7 +378,11 @@ public static class AdminEndpoints
|
|||
o.Id,
|
||||
o.OrderNo,
|
||||
o.OwnerId,
|
||||
OwnerUid = o.Owner != null ? o.Owner.Uid : "",
|
||||
OwnerNickname = o.Owner != null ? o.Owner.Nickname : "",
|
||||
o.RunnerId,
|
||||
RunnerUid = o.Runner != null ? o.Runner.Uid : "",
|
||||
RunnerNickname = o.Runner != null ? o.Runner.Nickname : "",
|
||||
OrderType = o.OrderType.ToString(),
|
||||
Status = o.Status.ToString(),
|
||||
o.ItemName,
|
||||
|
|
@ -505,17 +513,18 @@ public static class AdminEndpoints
|
|||
|
||||
var certifications = await query
|
||||
.OrderByDescending(c => c.CreatedAt)
|
||||
.Select(c => new AdminCertificationResponse
|
||||
.Select(c => new
|
||||
{
|
||||
Id = c.Id,
|
||||
UserId = c.UserId,
|
||||
RealName = c.RealName,
|
||||
Phone = c.Phone,
|
||||
c.Id,
|
||||
c.UserId,
|
||||
UserUid = c.User != null ? c.User.Uid : "",
|
||||
c.RealName,
|
||||
c.Phone,
|
||||
Status = c.Status.ToString(),
|
||||
CreatedAt = c.CreatedAt,
|
||||
ReviewedAt = c.ReviewedAt,
|
||||
UserNickname = c.User != null ? c.User.Nickname : null,
|
||||
UserPhone = c.User != null ? c.User.Phone : null
|
||||
c.CreatedAt,
|
||||
c.ReviewedAt,
|
||||
UserNickname = c.User != null ? c.User.Nickname : "",
|
||||
UserPhone = c.User != null ? c.User.Phone : ""
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
|
|
@ -583,9 +592,11 @@ public static class AdminEndpoints
|
|||
Status = o.Status.ToString(),
|
||||
o.Commission,
|
||||
OwnerId = o.OwnerId,
|
||||
OwnerUid = o.Owner!.Uid,
|
||||
OwnerNickname = o.Owner!.Nickname,
|
||||
OwnerAvatar = o.Owner!.AvatarUrl,
|
||||
RunnerId = o.RunnerId,
|
||||
RunnerUid = o.Runner!.Uid,
|
||||
RunnerNickname = o.Runner!.Nickname,
|
||||
RunnerAvatar = o.Runner!.AvatarUrl,
|
||||
CreatedAt = o.CreatedAt
|
||||
|
|
@ -601,9 +612,11 @@ public static class AdminEndpoints
|
|||
o.Status,
|
||||
o.Commission,
|
||||
o.OwnerId,
|
||||
OwnerUid = string.IsNullOrWhiteSpace(o.OwnerUid) ? o.OwnerId.ToString() : o.OwnerUid,
|
||||
OwnerNickname = string.IsNullOrWhiteSpace(o.OwnerNickname) ? $"用户{o.OwnerId}" : o.OwnerNickname,
|
||||
o.OwnerAvatar,
|
||||
o.RunnerId,
|
||||
RunnerUid = string.IsNullOrWhiteSpace(o.RunnerUid) ? o.RunnerId.ToString() : o.RunnerUid,
|
||||
RunnerNickname = string.IsNullOrWhiteSpace(o.RunnerNickname) ? $"用户{o.RunnerId}" : o.RunnerNickname,
|
||||
o.RunnerAvatar,
|
||||
o.CreatedAt
|
||||
|
|
|
|||
|
|
@ -94,6 +94,18 @@ public static class ConfigEndpoints
|
|||
});
|
||||
});
|
||||
|
||||
// 获取最低提现金额配置
|
||||
app.MapGet("/api/config/min-withdrawal", async (AppDbContext db) =>
|
||||
{
|
||||
var config = await db.SystemConfigs.FirstOrDefaultAsync(c => c.Key == "min_withdrawal");
|
||||
return Results.Ok(new ConfigResponse
|
||||
{
|
||||
Key = "min_withdrawal",
|
||||
Value = config?.Value ?? "1",
|
||||
UpdatedAt = config?.UpdatedAt ?? DateTime.MinValue
|
||||
});
|
||||
});
|
||||
|
||||
// 管理端获取指定配置
|
||||
app.MapGet("/api/admin/config/{key}", async (string key, AppDbContext db) =>
|
||||
{
|
||||
|
|
@ -113,7 +125,7 @@ public static class ConfigEndpoints
|
|||
var allowedKeys = new HashSet<string>
|
||||
{
|
||||
"qrcode", "agreement", "privacy", "runner_agreement", "withdrawal_guide", "freeze_days",
|
||||
"min_commission",
|
||||
"min_commission", "min_withdrawal",
|
||||
"page_banner_pickup", "page_banner_delivery", "page_banner_help", "page_banner_purchase", "page_banner_food",
|
||||
"page_banner_order-hall"
|
||||
};
|
||||
|
|
|
|||
|
|
@ -93,9 +93,14 @@ public static class EarningEndpoints
|
|||
if (userIdClaim == null) return Results.Unauthorized();
|
||||
var userId = int.Parse(userIdClaim.Value);
|
||||
|
||||
// 金额校验:最低 1 元
|
||||
if (request.Amount < 1.0m)
|
||||
return Results.BadRequest(new { code = 400, message = "提现金额不能低于1元" });
|
||||
// 金额校验:读取最低提现金额配置
|
||||
var minWithdrawalConfig = await db.SystemConfigs.FirstOrDefaultAsync(c => c.Key == "min_withdrawal");
|
||||
var minWithdrawal = 1.0m;
|
||||
if (minWithdrawalConfig != null && decimal.TryParse(minWithdrawalConfig.Value, out var configMinW))
|
||||
minWithdrawal = configMinW;
|
||||
|
||||
if (request.Amount < minWithdrawal)
|
||||
return Results.BadRequest(new { code = 400, message = $"提现金额不能低于{minWithdrawal}元" });
|
||||
|
||||
// 金额校验:小数点后最多 2 位
|
||||
if (request.Amount != Math.Round(request.Amount, 2))
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user