半自动+人工”链路已经落下第一版
This commit is contained in:
@@ -55,6 +55,7 @@ type ValidationState = {
|
||||
|
||||
const PROFILE_OPTIONS = [
|
||||
{ label: '腾讯领取兑换', value: 'tencent_claim_redeem' },
|
||||
{ label: '腾讯领取兑换(半自动+人工)', value: 'tencent_claim_assisted' },
|
||||
{ label: '人工发货', value: 'manual_review' },
|
||||
]
|
||||
|
||||
@@ -405,7 +406,7 @@ onMounted(loadConfigs)
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">说明</span>
|
||||
<span>每条规则同时定义“外部商品匹配条件”和“内部履约 SKU / 履约方式”。未命中的商品会进入人工处理。</span>
|
||||
<span>每条规则同时定义“外部商品匹配条件”和“内部履约 SKU / 履约方式”。未命中的商品会直接忽略,不再继续走后续履约动作。</span>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">字段怎么理解</span>
|
||||
@@ -424,7 +425,7 @@ onMounted(loadConfigs)
|
||||
</div>
|
||||
<div class="hint-item">
|
||||
<strong>履约方式</strong>
|
||||
<span>`腾讯领取兑换` 表示走自动库存链路,`人工发货` 表示落到人工处理。</span>
|
||||
<span>`腾讯领取兑换` 表示用户确认后自动兑换,`腾讯领取兑换(半自动+人工)` 表示用户先登录、客服确认角色后再自动兑换,`人工发货` 表示直接落到人工处理。</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -423,12 +423,12 @@ onMounted(loadInventoryItems)
|
||||
<article class="guide-item">
|
||||
<span class="guide-label">2. 库存预占</span>
|
||||
<h2>再按凭据类型找库存项</h2>
|
||||
<p>`tencent_claim_redeem` 当前会优先查找 `tencent_code`,适合三角洲行动这类腾讯兑换码自动领取场景。以后扩展别的履约档案时,也会按各自 requirement 匹配。</p>
|
||||
<p>`tencent_claim_redeem` 和 `tencent_claim_assisted` 当前都会优先查找 `tencent_code`,适合三角洲行动这类腾讯兑换码领取场景。以后扩展别的履约档案时,也会按各自 requirement 匹配。</p>
|
||||
</article>
|
||||
<article class="guide-item">
|
||||
<span class="guide-label">3. 履约结果</span>
|
||||
<h2>自动绑定或转人工</h2>
|
||||
<p>命中库存后任务会进入预占 / 发放链路;没命中库存、或配置走 `manual_review` 时,会落到人工处理。</p>
|
||||
<p>命中库存后任务会进入预占 / 发放链路;配置为 `tencent_claim_assisted` 时会进入“用户登录 + 客服确认 + 后端自动兑换”;配置走 `manual_review` 时,会落到人工处理。</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -8,17 +8,33 @@ import { clearAdminSession, getAdminRole, getAdminTokenExpiresAt, getAdminUserna
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
const router = useRouter()
|
||||
const isAdmin = computed(() => getAdminRole() === 'admin')
|
||||
const currentRole = computed(() => getAdminRole())
|
||||
const isAdmin = computed(() => currentRole.value === 'admin')
|
||||
const isOperator = computed(() => currentRole.value === 'operator')
|
||||
const roleLabel = computed(() => {
|
||||
if (currentRole.value === 'admin') {
|
||||
return '管理员'
|
||||
}
|
||||
|
||||
if (currentRole.value === 'support') {
|
||||
return '客服'
|
||||
}
|
||||
|
||||
return '普通运营'
|
||||
})
|
||||
const navItems = computed(() => {
|
||||
const baseItems = [
|
||||
{ to: '/admin/dashboard', label: '概览' },
|
||||
{ to: '/admin/orders', label: '订单' },
|
||||
{ to: '/admin/tasks', label: '任务' },
|
||||
{ to: '/admin/inventory', label: '库存' },
|
||||
{ to: '/admin/message-deliveries', label: '消息发送' },
|
||||
{ to: '/admin/webhook-events', label: 'Webhook' },
|
||||
]
|
||||
|
||||
if (isAdmin.value || isOperator.value) {
|
||||
baseItems.splice(3, 0, { to: '/admin/inventory', label: '库存' })
|
||||
baseItems.push({ to: '/admin/webhook-events', label: 'Webhook' })
|
||||
}
|
||||
|
||||
if (isAdmin.value) {
|
||||
baseItems.splice(1, 0, { to: '/admin/users', label: '用户' })
|
||||
baseItems.push({ to: '/admin/platform-shops', label: 'Agiso店铺' })
|
||||
@@ -54,7 +70,7 @@ async function submitLogout() {
|
||||
<span>当前账号</span>
|
||||
<strong>{{ getAdminUsername() || '未读取' }}</strong>
|
||||
<span>当前角色</span>
|
||||
<strong>{{ getAdminRole() === 'admin' ? '管理员' : '普通运营' }}</strong>
|
||||
<strong>{{ roleLabel }}</strong>
|
||||
<span>登录有效期</span>
|
||||
<strong>{{ formatAdminDateTime(getAdminTokenExpiresAt()) }}</strong>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue'
|
||||
import { showConfirm, showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
closeAdminTask,
|
||||
confirmAdminTaskAssistedRole,
|
||||
completeAdminTaskManualDispatch,
|
||||
redeemAdminTaskAssisted,
|
||||
fetchAdminTaskScreenshot,
|
||||
fetchAdminTaskDetail,
|
||||
markAdminTaskManualReview,
|
||||
@@ -31,6 +33,16 @@ const manualDispatchForm = reactive({
|
||||
deliveredCredential: '',
|
||||
resultMessage: '',
|
||||
})
|
||||
const canOperateTasks = computed(() => hasAdminRole('operator'))
|
||||
const canViewSensitiveTaskData = computed(() => Boolean(detail.value?.operations.canViewSensitiveTaskData))
|
||||
const claimUrl = computed(() => lastClaimUrl.value || detail.value?.claimToken?.claimUrl || '')
|
||||
const screenshotSectionTitle = computed(() => {
|
||||
if (detail.value?.task.status === 'redeemed') {
|
||||
return '结果截图'
|
||||
}
|
||||
|
||||
return detail.value?.review.required ? '客服复核截图' : '结果截图'
|
||||
})
|
||||
|
||||
async function loadDetail() {
|
||||
loading.value = true
|
||||
@@ -168,7 +180,10 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
<p>系统绑定:<AdminStatusTag :status="detail.task.systemBindingStatus" /> · 完整绑定:<AdminStatusTag :status="detail.task.userBindingStatus" /></p>
|
||||
<p>角色:{{ detail.task.roleName || '-' }} / {{ detail.task.roleId || '-' }}</p>
|
||||
<p>最后错误:{{ detail.task.lastError || '-' }}</p>
|
||||
<p v-if="lastClaimUrl">新领取链接:{{ lastClaimUrl }}</p>
|
||||
<p v-if="claimUrl">
|
||||
领取链接:
|
||||
<a class="claim-link" :href="claimUrl" target="_blank" rel="noreferrer">{{ claimUrl }}</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="action-row">
|
||||
@@ -182,7 +197,7 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
重试任务
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasAdminRole('admin')"
|
||||
v-if="detail.operations.canRegenerateClaimLink"
|
||||
:disabled="!detail.operations.canRegenerateClaimLink"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
@@ -191,7 +206,33 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
重发链接
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasAdminRole('admin')"
|
||||
v-if="detail.operations.canSupportConfirmRole"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="warning"
|
||||
@click="runAction(
|
||||
() => confirmAdminTaskAssistedRole(detail!.task.taskId),
|
||||
'角色已确认',
|
||||
`确认以客服身份锁定任务 ${detail!.task.taskNo} 当前识别到的角色信息吗?`
|
||||
)"
|
||||
>
|
||||
客服确认角色
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="detail.operations.canSupportRedeem"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="success"
|
||||
@click="runAction(
|
||||
() => redeemAdminTaskAssisted(detail!.task.taskId),
|
||||
'兑换任务已启动',
|
||||
`确认开始执行任务 ${detail!.task.taskNo} 的自动兑换吗?`
|
||||
)"
|
||||
>
|
||||
客服开始兑换
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canOperateTasks"
|
||||
:disabled="!detail.operations.canMarkManualReview"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
@@ -200,7 +241,7 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
转人工
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasAdminRole('admin') && detail.operations.canCompleteManualDispatch"
|
||||
v-if="canOperateTasks && detail.operations.canCompleteManualDispatch"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="success"
|
||||
@@ -209,7 +250,7 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
人工完成
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasAdminRole('admin') && detail.operations.canCompleteManualDispatch"
|
||||
v-if="canOperateTasks && detail.operations.canCompleteManualDispatch"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="danger"
|
||||
@@ -219,7 +260,7 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
履约失败
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasAdminRole('admin')"
|
||||
v-if="canOperateTasks"
|
||||
:disabled="!detail.operations.canClose"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
@@ -240,12 +281,20 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
<tr><th>履约状态</th><td><AdminStatusTag :status="detail.task.deliveryStatus || ''" /></td></tr>
|
||||
<tr><th>结果代码</th><td>{{ detail.task.resultCode || '-' }}</td></tr>
|
||||
<tr><th>结果说明</th><td>{{ detail.task.resultMessage || '-' }}</td></tr>
|
||||
<tr><th>库存凭据</th><td>{{ detail.inventory?.displayValue || '-' }}</td></tr>
|
||||
<tr><th>凭据类型</th><td>{{ detail.inventory?.credentialType || '-' }}</td></tr>
|
||||
<tr><th>库存状态</th><td><AdminStatusTag :status="detail.inventory?.status || ''" /></td></tr>
|
||||
<tr><th>Claim Token</th><td>{{ detail.claimToken?.token || '-' }}</td></tr>
|
||||
<tr v-if="canViewSensitiveTaskData"><th>库存凭据</th><td>{{ detail.inventory?.displayValue || '-' }}</td></tr>
|
||||
<tr v-if="canViewSensitiveTaskData"><th>凭据类型</th><td>{{ detail.inventory?.credentialType || '-' }}</td></tr>
|
||||
<tr v-if="canViewSensitiveTaskData"><th>库存状态</th><td><AdminStatusTag :status="detail.inventory?.status || ''" /></td></tr>
|
||||
<tr v-if="canViewSensitiveTaskData"><th>Claim Token</th><td>{{ detail.claimToken?.token || '-' }}</td></tr>
|
||||
<tr v-else><th>库存 / Token</th><td>客服账号不可查看库存凭据与原始领取 Token</td></tr>
|
||||
<tr><th>Token 状态</th><td><AdminStatusTag :status="detail.claimToken?.status || ''" /></td></tr>
|
||||
<tr><th>Token 过期</th><td>{{ formatAdminDateTime(detail.claimToken?.expiredAt) }}</td></tr>
|
||||
<tr>
|
||||
<th>领取链接</th>
|
||||
<td>
|
||||
<a v-if="claimUrl" class="claim-link" :href="claimUrl" target="_blank" rel="noreferrer">{{ claimUrl }}</a>
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><th>浏览器会话</th><td>{{ detail.task.browserSessionId || '-' }}</td></tr>
|
||||
<tr><th>用户打开链接</th><td>{{ formatAdminDateTime(detail.task.claimedAt) }}</td></tr>
|
||||
<tr><th>用户提交绑定</th><td>{{ formatAdminDateTime(detail.task.roleConfirmedAt) }}</td></tr>
|
||||
@@ -295,7 +344,7 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
</div>
|
||||
<div v-if="binding.invalidReason" class="binding-meta">作废原因:{{ binding.invalidReason }}</div>
|
||||
</td>
|
||||
<td>{{ binding.displayValue || '-' }}</td>
|
||||
<td>{{ canViewSensitiveTaskData ? (binding.displayValue || '-') : '客服不可见' }}</td>
|
||||
<td>
|
||||
<div>绑定 {{ formatAdminDateTime(binding.createdAt) }}</div>
|
||||
<div class="binding-meta">消费 {{ formatAdminDateTime(binding.consumedAt) }}</div>
|
||||
@@ -328,6 +377,7 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
<span>外部流水 / 单号</span>
|
||||
<el-input
|
||||
v-model="manualDispatchForm.deliveryReference"
|
||||
:disabled="!canOperateTasks"
|
||||
maxlength="120"
|
||||
placeholder="例如快递单号、平台消息回执号"
|
||||
/>
|
||||
@@ -336,6 +386,7 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
<span>已交付内容</span>
|
||||
<el-input
|
||||
v-model="manualDispatchForm.deliveredCredential"
|
||||
:disabled="!canOperateTasks"
|
||||
maxlength="500"
|
||||
placeholder="可填写人工发送的卡密、链接或关键信息"
|
||||
type="textarea"
|
||||
@@ -346,6 +397,7 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
<span>处理备注</span>
|
||||
<el-input
|
||||
v-model="manualDispatchForm.resultMessage"
|
||||
:disabled="!canOperateTasks"
|
||||
maxlength="500"
|
||||
placeholder="说明实际履约结果,失败时建议写清原因"
|
||||
type="textarea"
|
||||
@@ -379,10 +431,10 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
</section>
|
||||
|
||||
<section v-if="detail.screenshotUrl" class="table-card">
|
||||
<h3>结果截图</h3>
|
||||
<h3>{{ screenshotSectionTitle }}</h3>
|
||||
<p v-if="!screenshotPreviewUrl" class="screenshot-empty">截图加载中或当前不可读取。</p>
|
||||
<a v-else class="screenshot-link" :href="screenshotPreviewUrl" target="_blank" rel="noreferrer">打开原图</a>
|
||||
<img v-if="screenshotPreviewUrl" class="screenshot-preview" :src="screenshotPreviewUrl" alt="任务兑换截图" />
|
||||
<img v-if="screenshotPreviewUrl" class="screenshot-preview" :src="screenshotPreviewUrl" :alt="screenshotSectionTitle" />
|
||||
</section>
|
||||
</template>
|
||||
</section>
|
||||
@@ -392,6 +444,7 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
.admin-panel { display: grid; gap: 16px; }
|
||||
.panel-header h1 { margin: 0; color: #1d3555; }
|
||||
.panel-header p { margin: 8px 0 0; color: #64748b; }
|
||||
.claim-link { color: #175cd3; text-decoration: none; word-break: break-all; }
|
||||
.info-card,.table-card,.empty-block,.error-copy {
|
||||
padding: 18px; border-radius: 20px; background: rgba(255,255,255,.94); border: 1px solid rgba(86,108,138,.1);
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ function handleActionCommand(command: `${AdminTaskActionKey}:${number}`) {
|
||||
重试
|
||||
</el-button>
|
||||
<div
|
||||
v-if="hasAdminRole('admin')"
|
||||
v-if="hasAdminRole('operator')"
|
||||
class="more-action-wrap"
|
||||
@click.stop
|
||||
>
|
||||
|
||||
@@ -37,6 +37,18 @@ function isCurrentUser(userId: number) {
|
||||
return getAdminUserId() === userId
|
||||
}
|
||||
|
||||
function formatRoleLabel(role: AdminRole) {
|
||||
if (role === 'admin') {
|
||||
return '管理员'
|
||||
}
|
||||
|
||||
if (role === 'support') {
|
||||
return '客服'
|
||||
}
|
||||
|
||||
return '普通运营'
|
||||
}
|
||||
|
||||
async function loadUsers(page = pagination.value.page) {
|
||||
if (!hasAdminRole('admin')) {
|
||||
loading.value = false
|
||||
@@ -90,11 +102,13 @@ async function submitCreate() {
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleRole(item: AdminUserListItem) {
|
||||
const nextRole: AdminRole = item.role === 'admin' ? 'operator' : 'admin'
|
||||
async function updateRole(item: AdminUserListItem, nextRole: AdminRole) {
|
||||
if (item.role === nextRole) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await showConfirm(`确认将 ${item.username} 调整为${nextRole === 'admin' ? '管理员' : '普通运营'}吗?`, '确认操作', {
|
||||
await showConfirm(`确认将 ${item.username} 调整为${formatRoleLabel(nextRole)}吗?`, '确认操作', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '继续执行',
|
||||
cancelButtonText: '取消',
|
||||
@@ -202,6 +216,7 @@ onMounted(loadUsers)
|
||||
<input v-model="createPassword" class="text-input" type="password" placeholder="新密码,至少 8 位" />
|
||||
<select v-model="createRole" class="text-input select-input">
|
||||
<option value="operator">普通运营</option>
|
||||
<option value="support">客服</option>
|
||||
<option value="admin">管理员</option>
|
||||
</select>
|
||||
<el-button :loading="creating" round type="primary" @click="submitCreate">新增用户</el-button>
|
||||
@@ -228,7 +243,7 @@ onMounted(loadUsers)
|
||||
<tr v-for="item in items" :key="item.userId">
|
||||
<td>{{ item.userId }}</td>
|
||||
<td>{{ item.username }}</td>
|
||||
<td>{{ item.role === 'admin' ? '管理员' : '普通运营' }}</td>
|
||||
<td>{{ formatRoleLabel(item.role) }}</td>
|
||||
<td><AdminStatusTag :status="item.status" /></td>
|
||||
<td>{{ formatAdminDateTime(item.createdAt) }}</td>
|
||||
<td>{{ formatAdminDateTime(item.updatedAt) }}</td>
|
||||
@@ -238,9 +253,25 @@ onMounted(loadUsers)
|
||||
:loading="actionLoadingId === item.userId"
|
||||
link
|
||||
:disabled="isCurrentUser(item.userId)"
|
||||
@click="toggleRole(item)"
|
||||
@click="updateRole(item, 'admin')"
|
||||
>
|
||||
{{ item.role === 'admin' ? '设为运营' : '设为管理员' }}
|
||||
设为管理员
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="actionLoadingId === item.userId"
|
||||
link
|
||||
:disabled="isCurrentUser(item.userId)"
|
||||
@click="updateRole(item, 'operator')"
|
||||
>
|
||||
设为运营
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="actionLoadingId === item.userId"
|
||||
link
|
||||
:disabled="isCurrentUser(item.userId)"
|
||||
@click="updateRole(item, 'support')"
|
||||
>
|
||||
设为客服
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="actionLoadingId === item.userId"
|
||||
|
||||
@@ -62,6 +62,10 @@ const helperText = computed(() => {
|
||||
return '正在加载领取任务'
|
||||
}
|
||||
|
||||
if (task.value.requiresSupportReview) {
|
||||
return `订单 ${order.value?.platformOrderId || '-'} · 登录后请联系人工客服继续确认和兑换`
|
||||
}
|
||||
|
||||
return `订单 ${order.value?.platformOrderId || '-'} · 任务 ${task.value.taskNo}`
|
||||
})
|
||||
</script>
|
||||
@@ -135,7 +139,7 @@ const helperText = computed(() => {
|
||||
:can-confirm-role="canConfirmRole"
|
||||
confirm-action-label="确认当前角色并继续"
|
||||
:confirm-action-loading="roleConfirmLoading"
|
||||
:confirm-action-visible="!roleConfirmed"
|
||||
:confirm-action-visible="!roleConfirmed && !task?.requiresSupportReview"
|
||||
:confirm-readonly="true"
|
||||
:hide-redeem-form="true"
|
||||
:login-type-label="loginTypeLabel"
|
||||
|
||||
Reference in New Issue
Block a user