聊天,金额,UI
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
18631081161 2026-04-01 21:26:13 +08:00
parent 319c46f29c
commit 5a04e003ca
27 changed files with 319 additions and 83 deletions

View File

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

View File

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

View File

@ -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()
])
})

View File

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

View File

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

View File

@ -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())}`
}

View File

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

View File

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

View File

@ -1,5 +1,5 @@
{
"name" : "校侠",
"name" : "校跑侠校园服务",
"appid" : "__UNI__F1854F8",
"description" : "",
"versionName" : "1.0.0",

View File

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

View File

@ -72,7 +72,7 @@
<!-- 提交按钮 -->
<view class="submit-section">
<button class="submit-btn" @click="onSubmit" :loading="submitting" :disabled="submitting">
支付佣金 确定下单
确定下单
</button>
</view>
</view>

View File

@ -442,6 +442,10 @@ export default {
border: none;
}
.modal-btn.confirm::after {
border: none;
}
.modal-btn.confirm {
background-color: #FAD146;
color: #333333;

View File

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

View File

@ -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() {

View File

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

View File

@ -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;
}

View File

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

View File

@ -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' })
//

View File

@ -683,6 +683,10 @@ export default {
border: none;
}
.modal-btn::after {
border: none;
}
.modal-btn.cancel {
background-color: #f5f5f5;
color: #666666;

View File

@ -72,7 +72,7 @@
<!-- 提交按钮 -->
<view class="submit-section">
<button class="submit-btn" @click="onSubmit" :loading="submitting" :disabled="submitting">
支付佣金 确定下单
确定下单
</button>
</view>
</view>

View File

@ -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() {

View File

@ -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 */

View File

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

View File

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

View File

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

View File

@ -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"
};

View File

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