feat(ECR-009): Ops-D order filters, plan display prices, refund status

LOOP-RUN-002 sample: admin order multi-filter, membership display
price catalog, read-only refund_status. No real payment or RBAC.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-07 02:45:32 +08:00
co-authored by Cursor
parent 7ab9add5dd
commit ca3b13554c
37 changed files with 2169 additions and 25 deletions
+29 -2
View File
@@ -4,6 +4,11 @@ import { adminApi } from '@/api/client'
const loading = ref(false)
const error = ref('')
const status = ref('')
const kind = ref('')
const from = ref('')
const to = ref('')
const userId = ref('')
const items = ref<
Array<{
id: string
@@ -12,6 +17,7 @@ const items = ref<
plan?: string
amount_cents: number
status: string
refund_status: string
created_at: string
}>
>([])
@@ -20,7 +26,13 @@ async function load() {
loading.value = true
error.value = ''
try {
const res = await adminApi.orders()
const res = await adminApi.orders({
status: status.value || undefined,
kind: kind.value || undefined,
from: from.value || undefined,
to: to.value || undefined,
user_id: userId.value || undefined,
})
items.value = res.items || []
} catch (e) {
error.value = e instanceof Error ? e.message : '加载失败'
@@ -35,13 +47,23 @@ onMounted(load)
<template>
<section>
<h1>订单</h1>
<div class="filters card">
<label>状态 <input v-model="status" placeholder="paid / pending" /></label>
<label>类型 <input v-model="kind" placeholder="membership" /></label>
<label> <input v-model="from" type="date" /></label>
<label> <input v-model="to" type="date" /></label>
<label>用户 ID <input v-model="userId" class="wide" /></label>
<button class="btn" type="button" @click="load">筛选</button>
</div>
<p v-if="loading" class="muted">加载中</p>
<p v-else-if="error" class="err">{{ error }}</p>
<div v-else class="card">
<p v-if="!items.length" class="muted">暂无订单</p>
<table v-else>
<thead>
<tr><th>ID</th><th>用户</th><th>类型</th><th>状态</th><th>金额</th><th>时间</th></tr>
<tr>
<th>ID</th><th>用户</th><th>类型</th><th>状态</th><th>退款</th><th>金额</th><th>时间</th>
</tr>
</thead>
<tbody>
<tr v-for="o in items" :key="o.id">
@@ -49,6 +71,7 @@ onMounted(load)
<td>{{ o.user_id }}</td>
<td>{{ o.kind }} {{ o.plan || '' }}</td>
<td>{{ o.status }}</td>
<td>{{ o.refund_status }}</td>
<td>{{ (o.amount_cents / 100).toFixed(2) }}</td>
<td>{{ o.created_at }}</td>
</tr>
@@ -60,4 +83,8 @@ onMounted(load)
<style scoped>
h1 { margin: 0 0 1rem; font-size: 1.35rem; }
.filters { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: end; margin-bottom: 1rem; }
.filters label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.85rem; }
.filters input { min-width: 8rem; }
.filters input.wide { min-width: 16rem; }
</style>
+78
View File
@@ -0,0 +1,78 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { adminApi } from '@/api/client'
const loading = ref(false)
const saving = ref(false)
const error = ref('')
const ok = ref('')
const items = ref<Array<{ plan: string; display_cents: number; updated_at?: string }>>([])
async function load() {
loading.value = true
error.value = ''
try {
const res = await adminApi.planPrices()
items.value = (res.items || []).map((i) => ({ ...i }))
} catch (e) {
error.value = e instanceof Error ? e.message : '加载失败'
} finally {
loading.value = false
}
}
async function save() {
saving.value = true
error.value = ''
ok.value = ''
try {
await adminApi.savePlanPrices(
items.value.map((i) => ({ plan: i.plan, display_cents: Number(i.display_cents) })),
)
ok.value = '已保存展示价(不影响历史订单金额)'
await load()
} catch (e) {
error.value = e instanceof Error ? e.message : '保存失败'
} finally {
saving.value = false
}
}
onMounted(load)
</script>
<template>
<section>
<h1>套餐展示价</h1>
<p class="muted">仅目录展示价不改已支付订单的 amount_cents</p>
<p v-if="loading" class="muted">加载中</p>
<p v-else-if="error" class="err">{{ error }}</p>
<p v-if="ok" class="ok">{{ ok }}</p>
<div v-if="!loading && !error" class="card">
<table>
<thead>
<tr><th>套餐</th><th>展示价</th><th>更新时间</th></tr>
</thead>
<tbody>
<tr v-for="row in items" :key="row.plan">
<td>{{ row.plan }}</td>
<td>
<input v-model.number="row.display_cents" type="number" min="0" />
</td>
<td class="muted">{{ row.updated_at || '—' }}</td>
</tr>
</tbody>
</table>
<button class="btn" type="button" :disabled="saving" @click="save">
{{ saving ? '保存中' : '保存' }}
</button>
</div>
</section>
</template>
<style scoped>
h1 { margin: 0 0 0.5rem; font-size: 1.35rem; }
.ok { color: #1a7f37; }
input[type='number'] { width: 8rem; }
.btn { margin-top: 1rem; }
</style>