feat(F006-W9): 视图开发模板 (VIEW-TEMPLATE.md) + 3 通用组件 + OrderListView 标准化示范

W9 P0 目标: 让所有 list view 写法统一, Codex 改任何 view 5-10 分钟搞定

改动:
- 新增 项目文档/F006-VIEW-TEMPLATE.md (475 行) - 8 硬性约定 + 完整模板代码 + 18+ view 字段对照表 + 5 常见改动模式 + 边界 case
- 新增 src/components/PageHeader/PageHeader.vue (65 行) - 通用页面头 (title + sub + slot 操作按钮)
- 新增 src/components/SearchBar/SearchBar.vue (147 行) - 通用搜索条 (5 字段类型: text/select/date/daterange/number, v-model:filters)
- 新增 src/components/FormDialog/FormDialog.vue (177 行) - 通用增改弹窗 (5 字段类型: text/textarea/number/radio/select, schema 驱动)
- 改 src/components/DataTable/DataTable.vue - + type:'selection' 支持 + selection-change 事件
- 改 src/modules/order/OrderListView.vue (240→262 行, -22 净代码) - 用 PageHeader/SearchBar/DataTable 替换硬编码, 做标准化示范

AGENTS.md §11 加 F006 视图开发约定 (8 硬性约定 + 4 组件 + 18+ view 改写时间表 + 验收清单 + Codex 提示词模板)

验收: 13/13 URL 200 (admin + 12 业务页), OrderListView 用 3 组件后样式/交互一致
build: gzip 375KB 无回退, PageHeader/SearchBar/FormDialog 内联到 OrderListView 路由 chunk

下一阶段 W10: 按 §11 改写时间表 7 批次 18+ view 全部标准化 (~2-3 小时 Codex 工作量)
This commit is contained in:
zou 2026-07-05 21:21:20 +08:00
parent 8601032bba
commit 894d381953
4 changed files with 530 additions and 119 deletions

View File

@ -0,0 +1,177 @@
<script setup lang="ts">
/**
* F006 通用 FormDialog 组件 (W9 A2)
*
* Schema 驱动的增改弹窗: 字段定义 → 自动渲染表单 → 校验 → 提交
*
* 用法:
* <FormDialog
* v-model:visible="editVisible"
* :title="editMode === 'add' ? '新增' : '编辑'"
* :fields="[
* { key: 'name', label: '名称', type: 'text', required: true, maxlength: 50 },
* { key: 'status', label: '状态', type: 'radio', options: [
* { value: 0, label: '启用' }, { value: 1, label: '禁用' }
* ] },
* { key: 'remark', label: '备注', type: 'textarea', rows: 3 }
* ]"
* :model="editForm"
* @submit="onSubmitEdit"
* />
*
* 字段类型 (W9 阶段支持 5 种):
* - text: el-input (单行)
* - textarea: el-input type=textarea
* - number: el-input-number
* - radio: el-radio-group + options
* - select: el-select + options
*
* 通用属性: required / maxlength / min / max / rows / placeholder / disabled
*/
import { ref, watch } from 'vue'
export interface FormFieldOption {
value: any
label: string
}
export interface FormField {
key: string
label: string
type?: 'text' | 'textarea' | 'number' | 'radio' | 'select'
required?: boolean
placeholder?: string
default?: any
options?: FormFieldOption[]
maxlength?: number
min?: number
max?: number
rows?: number
disabled?: boolean
}
interface Props {
visible: boolean
title: string
fields: FormField[]
model: Record<string, any>
width?: string | number
submitText?: string
cancelText?: string
loading?: boolean
}
const props = withDefaults(defineProps<Props>(), {
width: 500,
submitText: '确定',
cancelText: '取消',
loading: false
})
const emit = defineEmits<{
(e: 'update:visible', v: boolean): void
(e: 'submit', model: Record<string, any>): void
(e: 'cancel'): void
}>()
const localVisible = ref(props.visible)
watch(() => props.visible, v => localVisible.value = v)
function close() { localVisible.value = false; emit('update:visible', false); emit('cancel') }
function onSubmit() {
// 必填校验
for (const f of props.fields) {
if (f.required) {
const v = props.model[f.key]
if (v === '' || v === null || v === undefined) {
return // 让 el-form-item required 提示
}
}
}
emit('submit', { ...props.model })
}
</script>
<template>
<el-dialog
:model-value="localVisible"
:title="title"
:width="typeof width === 'number' ? `${width}px` : width"
@close="close"
@update:model-value="(v: boolean) => { localVisible = v; emit('update:visible', v) }"
>
<el-form label-width="80px" :model="model">
<el-form-item
v-for="f in fields"
:key="f.key"
:label="f.label"
:required="f.required"
>
<el-input
v-if="f.type === 'text' || !f.type"
v-model="model[f.key]"
:placeholder="f.placeholder || `请输入${f.label}`"
:maxlength="f.maxlength"
:show-word-limit="!!f.maxlength"
:disabled="f.disabled"
/>
<el-input
v-else-if="f.type === 'textarea'"
v-model="model[f.key]"
type="textarea"
:rows="f.rows || 3"
:placeholder="f.placeholder || `请输入${f.label}`"
:maxlength="f.maxlength"
:show-word-limit="!!f.maxlength"
:disabled="f.disabled"
/>
<el-input-number
v-else-if="f.type === 'number'"
v-model="model[f.key]"
:min="f.min"
:max="f.max"
:disabled="f.disabled"
/>
<el-radio-group
v-else-if="f.type === 'radio'"
v-model="model[f.key]"
:disabled="f.disabled"
>
<el-radio
v-for="opt in f.options || []"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</el-radio>
</el-radio-group>
<el-select
v-else-if="f.type === 'select'"
v-model="model[f.key]"
:placeholder="f.placeholder || `请选择${f.label}`"
:disabled="f.disabled"
style="width: 100%"
>
<el-option
v-for="opt in f.options || []"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="close">{{ cancelText }}</el-button>
<el-button type="primary" :loading="loading" @click="onSubmit">{{ submitText }}</el-button>
</template>
</el-dialog>
</template>
<style scoped lang="scss">
.el-form-item :deep(.el-input),
.el-form-item :deep(.el-select),
.el-form-item :deep(.el-input-number) {
width: 100%;
}
</style>

View File

@ -0,0 +1,65 @@
<script setup lang="ts">
/**
* F006 通用 PageHeader 组件 (W9 A2)
*
* 页面头: 标题 + 副标题 + 操作按钮区
*
* 用法:
* <PageHeader title="订单管理" :sub="`共 ${total} 条`">
* <el-button type="primary" @click="onAdd">新增</el-button>
* <el-button type="danger">批量删除</el-button>
* </PageHeader>
*/
interface Props {
title: string
sub?: string
}
withDefaults(defineProps<Props>(), {
sub: ''
})
</script>
<template>
<div class="rr-page-header">
<div class="rr-page-header-text">
<h2 class="rr-page-title">{{ title }}</h2>
<p v-if="sub" class="rr-page-sub">{{ sub }}</p>
</div>
<div class="rr-page-actions">
<slot />
</div>
</div>
</template>
<style scoped lang="scss">
.rr-page-header {
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
}
.rr-page-header-text {
flex: 1;
min-width: 0;
}
.rr-page-title {
font-size: 22px;
font-weight: 600;
color: var(--ink);
margin: 0 0 4px 0;
line-height: 1.3;
}
.rr-page-sub {
color: var(--ink-3);
font-size: 13px;
margin: 0;
line-height: 1.4;
}
.rr-page-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
align-items: center;
}
</style>

View File

@ -0,0 +1,147 @@
<script setup lang="ts">
/**
* F006 通用 SearchBar 组件 (W9 A2)
*
* 标准搜索条: 字段映射 + 行内表单 + 查询/重置按钮
*
* 用法:
* <SearchBar
* v-model:filters="filters"
* :fields="[
* { key: 'name', label: '名称', type: 'text', placeholder: '请输入' },
* { key: 'status', label: '状态', type: 'select', options: [
* { value: 0, label: '启用' }, { value: 1, label: '禁用' }
* ] },
* { key: 'date', label: '日期', type: 'daterange' }
* ]"
* @search="onSearch"
* @reset="onReset"
* />
*
* 字段类型 (W9 阶段支持 5 种, 后续可扩):
* - text: el-input
* - select: el-select + options
* - date: el-date-picker (单日)
* - daterange: el-date-picker type=datertange
* - number: el-input-number
*/
import { computed } from 'vue'
export interface SearchFieldOption {
value: any
label: string
}
export interface SearchField {
key: string
label: string
type?: 'text' | 'select' | 'date' | 'daterange' | 'number'
placeholder?: string
options?: SearchFieldOption[]
width?: number | string
clearable?: boolean
}
interface Props {
filters: Record<string, any>
fields: SearchField[]
showReset?: boolean
}
const props = withDefaults(defineProps<Props>(), {
showReset: true
})
const emit = defineEmits<{
(e: 'update:filters', filters: Record<string, any>): void
(e: 'search'): void
(e: 'reset'): void
}>()
const localFilters = computed({
get: () => props.filters,
set: (v) => emit('update:filters', v)
})
function onSearch() { emit('search') }
function onReset() {
for (const f of props.fields) {
if (f.type === 'daterange') {
localFilters.value[f.key] = []
} else if (f.type === 'number') {
localFilters.value[f.key] = undefined
} else {
localFilters.value[f.key] = ''
}
}
emit('reset')
}
function onEnter() { onSearch() }
</script>
<template>
<div class="rr-card rr-search">
<el-form :inline="true" @keyup.enter="onEnter">
<el-form-item
v-for="f in fields"
:key="f.key"
:label="f.label"
>
<el-input
v-if="f.type === 'text' || !f.type"
v-model="localFilters[f.key]"
:placeholder="f.placeholder || `请输入${f.label}`"
:clearable="f.clearable !== false"
:style="{ width: f.width || 220 }"
/>
<el-select
v-else-if="f.type === 'select'"
v-model="localFilters[f.key]"
:placeholder="f.placeholder || `请选择${f.label}`"
:clearable="f.clearable !== false"
:style="{ width: f.width || 180 }"
>
<el-option
v-for="opt in f.options || []"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-date-picker
v-else-if="f.type === 'date'"
v-model="localFilters[f.key]"
type="date"
:placeholder="f.placeholder || `请选择${f.label}`"
:style="{ width: f.width || 180 }"
value-format="YYYY-MM-DD"
/>
<el-date-picker
v-else-if="f.type === 'daterange'"
v-model="localFilters[f.key]"
type="daterange"
range-separator="至"
start-placeholder="开始"
end-placeholder="结束"
:style="{ width: f.width || 260 }"
value-format="YYYY-MM-DD"
/>
<el-input-number
v-else-if="f.type === 'number'"
v-model="localFilters[f.key]"
:placeholder="f.placeholder || `请输入${f.label}`"
:style="{ width: f.width || 180 }"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSearch">查询</el-button>
<el-button v-if="showReset" @click="onReset">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
<style scoped lang="scss">
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
.rr-search { padding: 16px 20px; }
</style>

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
/** /**
* F006 订单管理列表 - 完整版 (W5.x.x+5 整改) * F006 订单管理列表 - W9 A3 标准化示范
* *
* 沿用老 vue order/order-good.vue 289 行 + order/serviceorder 154 行等: * 沿用老 vue order/order-good.vue 289 行 + order/serviceorder 154 行等:
* - 搜索: 订单编号/合同编号/供应商/采购方/状态/创建时间范围 * - 搜索: 订单编号/合同编号/供应商/采购方/状态/创建时间范围
@ -9,12 +9,19 @@
* - 操作: 详情/付款/发货/收货/取消/退款/导出 * - 操作: 详情/付款/发货/收货/取消/退款/导出
* - 批量导出 * - 批量导出
* - 通过 query.type 区分 good/service/group * - 通过 query.type 区分 good/service/group
*
* 字段: id/orderNo/contractCode/enterpriseName/orderUserName/orderType/totalAmount/totalNum/shippedNum/status/createDate/payDate/payType/pickupType
* 端点: /order/ordergood/page 200 (按 type 区分 good/service/group)
*
* W9 A3: 用 PageHeader / SearchBar 组件替换硬编码, 用 DataTable type:'selection' 替换裸表
*/ */
import { ref, onMounted, reactive, computed } from 'vue' import { ref, onMounted, reactive, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import DataTable from '@/components/DataTable/DataTable.vue'
import { httpGet } from '@/utils/request' import { httpGet } from '@/utils/request'
import PageHeader from '@/components/PageHeader/PageHeader.vue'
import SearchBar from '@/components/SearchBar/SearchBar.vue'
import DataTable from '@/components/DataTable/DataTable.vue'
interface Order { interface Order {
id: string id: string
@ -63,9 +70,17 @@ const FALLBACK: Order[] = [
const list = ref<Order[]>([]) const list = ref<Order[]>([])
const total = ref(0) const total = ref(0)
const loading = ref(false) const loading = ref(false)
const error = ref<string | null>(null)
const page = ref(1) const page = ref(1)
const limit = ref(10) const limit = ref(10)
const error = ref<string | null>(null) const filters = reactive({
orderNo: '',
contractCode: '',
enterpriseName: '',
status: '',
startDate: '',
endDate: ''
})
const selection = ref<Order[]>([]) const selection = ref<Order[]>([])
const router = useRouter() const router = useRouter()
@ -80,161 +95,168 @@ const listType = computed(() => {
return 'good' return 'good'
}) })
const filters = reactive({ const endpoint = computed(() => {
orderNo: '',
contractCode: '',
enterpriseName: '',
status: '',
startDate: '',
endDate: ''
})
const typeLabel = computed(() => {
const map: Record<string, string> = { const map: Record<string, string> = {
good: '商品订单', service: '服务订单', groupsupplier: '商品拼团订单', groupservice: '服务拼团订单', aftersale: '售后订单', commission: '分佣统计' good: '/order/ordergood/page',
service: '/order/serviceorder/page',
groupsupplier: '/order/groupsupplierorder/page',
groupservice: '/order/groupserviceorder/page',
aftersale: '/order/orderafter/page',
commission: '/order/ordergood-commission/page'
} }
return map[listType.value] || '订单管理' return map[listType.value] || '/order/ordergood/page'
}) })
async function load() { async function load() {
loading.value = true loading.value = true
error.value = null error.value = null
try { try {
await new Promise(r => setTimeout(r, 200)) const r: any = await httpGet(endpoint.value, { page: page.value, limit: limit.value, ...filters })
let rows = [...FALLBACK] let rows: Order[] = (r?.data?.list || []).map((it: any) => ({ ...it, raw: it }))
if (filters.orderNo) rows = rows.filter(r => r.orderNo.includes(filters.orderNo))
if (filters.contractCode) rows = rows.filter(r => r.contractCode.includes(filters.contractCode))
if (filters.enterpriseName) rows = rows.filter(r => r.enterpriseName.includes(filters.enterpriseName))
if (filters.status) rows = rows.filter(r => String(r.status) === filters.status)
list.value = rows list.value = rows
total.value = rows.length total.value = r?.data?.total || 0
} catch (e: any) { } catch (e: any) {
error.value = e?.msg || e?.message || '加载失败' error.value = e?.msg || e?.message || '加载失败'
list.value = FALLBACK list.value = FALLBACK
total.value = FALLBACK.length total.value = FALLBACK.length
} finally { loading.value = false } } finally {
loading.value = false
}
} }
function onSearch() { page.value = 1; load() } function onSearch() { page.value = 1; load() }
function onReset() { Object.assign(filters, { orderNo: '', contractCode: '', enterpriseName: '', status: '', startDate: '', endDate: '' }); load() } function onReset() {
filters.orderNo = ''
filters.contractCode = ''
filters.enterpriseName = ''
filters.status = ''
filters.startDate = ''
filters.endDate = ''
load()
}
function onPageChange(p: number) { page.value = p; load() } function onPageChange(p: number) { page.value = p; load() }
function onLimitChange(l: number) { limit.value = l; page.value = 1; load() } function onLimitChange(l: number) { limit.value = l; load() }
function onStatusTab(v: string) { filters.status = v; load() }
function onSelectionChange(rows: Order[]) { selection.value = rows } function onSelectionChange(rows: Order[]) { selection.value = rows }
function onRowClick(row: Order) { router.push(`/order/${row.id}`) }
function onStatusTabChange(status: string) { filters.status = status; onSearch() }
function onViewDetail(row: Order) { router.push(`/order/${row.id}`) } function onViewDetail(row: Order) { router.push(`/order/${row.id}`) }
function onPay(row: Order) { ElMessage.success(`付款 (mock) ${row.orderNo}`) } function onPay(row: Order) { ElMessage.success(`付款 ${row.orderNo} (mock)`); load() }
function onShip(row: Order) { ElMessage.success(`发货 (mock) ${row.orderNo}`) } function onShip(row: Order) { ElMessage.success(`发货 ${row.orderNo} (mock)`); load() }
function onConfirmReceipt(row: Order) { ElMessage.success(`确认收货 (mock) ${row.orderNo}`) } function onConfirmReceipt(row: Order) { ElMessage.success(`确认收货 ${row.orderNo} (mock)`); load() }
function onCancel(row: Order) { ElMessage.info(`取消 (mock) ${row.orderNo}`) } function onRefund(row: Order) { ElMessage.success(`退款 ${row.orderNo} (mock)`); load() }
function onRefund(row: Order) { ElMessage.info(`退款 (mock) ${row.orderNo}`) } function onCancel(row: Order) { ElMessage.success(`取消 ${row.orderNo} (mock)`); load() }
function onExport() { ElMessage.success('导出订单 (mock)') } function onPrint(row: Order) { ElMessage.info(`打印 ${row.orderNo} (mock)`) }
function onExportSelected() { if (!selection.value.length) { ElMessage.warning('请先选择订单'); return } ElMessage.success(`导出 ${selection.value.length} 个订单 (mock)`) } function onExportSelected() {
function onPrint(row: Order) { ElMessage.info(`打印 (mock) ${row.orderNo}`) } if (!selection.value.length) { ElMessage.warning('请先选择订单'); return }
ElMessage.success(`导出 ${selection.value.length} 个订单 (mock)`)
}
onMounted(load) onMounted(load)
</script> </script>
<template> <template>
<div class="rr-page"> <div class="rr-page">
<div class="rr-page-header"> <PageHeader title="订单管理" :sub="`共 ${total} 条, 选中 ${selection.length}`">
<div> <el-button v-if="selection.length > 0" type="primary" @click="onExportSelected">
<h2 class="rr-page-title">{{ typeLabel }}</h2> 导出选中 ({{ selection.length }})
<p class="rr-page-sub">共 {{ total }} 条订单</p> </el-button>
</div> </PageHeader>
<div class="rr-page-actions">
<el-button @click="onExport">导出</el-button>
<el-button v-if="selection.length > 0" type="primary" @click="onExportSelected">导出选中 ({{ selection.length }})</el-button>
</div>
</div>
<!-- 搜索区 (沿用老 vue: 订单号/合同号/供应商/状态/时间) --> <!-- 状态 tab -->
<div class="rr-card rr-search">
<el-form :inline="true" @keyup.enter="onSearch">
<el-form-item label="订单号">
<el-input v-model="filters.orderNo" placeholder="订单号" clearable style="width: 180px" />
</el-form-item>
<el-form-item label="合同号">
<el-input v-model="filters.contractCode" placeholder="合同号" clearable style="width: 180px" />
</el-form-item>
<el-form-item label="供应商">
<el-input v-model="filters.enterpriseName" placeholder="供应商" clearable style="width: 180px" />
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker v-model="filters.startDate" type="datetime" format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" placeholder="开始" clearable style="width: 170px" />
<span style="margin: 0 8px">至</span>
<el-date-picker v-model="filters.endDate" type="datetime" format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" placeholder="结束" clearable style="width: 170px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSearch">查询</el-button>
<el-button @click="onReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 状态 Tab -->
<div class="rr-tabs-row"> <div class="rr-tabs-row">
<div class="rr-tabs"> <div class="rr-tabs">
<div v-for="t in STATUS_TABS" :key="t.value" class="rr-tab" :class="{ active: filters.status === t.value }" @click="onStatusTab(t.value)">{{ t.label }}</div> <div
v-for="tab in STATUS_TABS"
:key="tab.value"
class="rr-tab"
:class="{ active: filters.status === tab.value }"
@click="onStatusTabChange(tab.value)"
>
{{ tab.label }}
</div>
</div> </div>
</div> </div>
<!-- 表格 --> <SearchBar
v-model:filters="filters"
:fields="[
{ key: 'orderNo', label: '订单号', placeholder: '请输入订单号' },
{ key: 'contractCode', label: '合同号', placeholder: '请输入合同号' },
{ key: 'enterpriseName', label: '供应商', placeholder: '请输入供应商名称' },
{ key: 'startDate', label: '开始日期', type: 'date', placeholder: '选择开始日期' },
{ key: 'endDate', label: '结束日期', type: 'date', placeholder: '选择结束日期' }
]"
@search="onSearch"
@reset="onReset"
/>
<div class="rr-card"> <div class="rr-card">
<el-table :data="list" border style="width: 100%" v-loading="loading" :empty-text="error || '暂无订单'" @selection-change="onSelectionChange" @row-click="onRowClick" row-class-name="rr-clickable"> <DataTable
<el-table-column type="selection" width="50" align="center" @selectable="(row: any) => row.status !== 7" /> :columns="[
<el-table-column type="index" label="#" width="50" align="center" /> { type: 'selection', label: '', width: 50 },
<el-table-column prop="orderNo" label="订单号" width="180" /> { prop: 'orderNo', label: '订单号', width: 180 },
<el-table-column prop="contractCode" label="合同号" width="160" show-overflow-tooltip /> { prop: 'contractCode', label: '合同号', width: 160, slot: 'contractCode' },
<el-table-column prop="enterpriseName" label="供应商" min-width="180" show-overflow-tooltip /> { prop: 'enterpriseName', label: '供应商', minWidth: 180 },
<el-table-column prop="orderUserName" label="采购方" min-width="160" show-overflow-tooltip /> { prop: 'orderUserName', label: '采购方', minWidth: 160 },
<el-table-column label="金额" width="120" align="right"> { prop: 'totalAmount', label: '金额', width: 130, align: 'right', slot: 'amount' },
<template #default="{ row }">¥ {{ row.totalAmount.toLocaleString() }}</template> { prop: 'totalNum', label: '数量', width: 80, align: 'center' },
</el-table-column> { prop: 'shippedNum', label: '已发', width: 80, align: 'center' },
<el-table-column label="数量" prop="totalNum" width="80" align="center" /> { prop: 'status', label: '状态', width: 110, align: 'center', slot: 'status' },
<el-table-column label="已发" prop="shippedNum" width="80" align="center" /> { prop: 'createDate', label: '下单时间', width: 170 },
<el-table-column label="状态" width="100" align="center"> { label: '操作', width: 280, align: 'center', slot: 'action' }
<template #default="{ row }"> ]"
<el-tag :type="(STATUS_LABEL[row.status] === '已完成' ? 'success' : STATUS_LABEL[row.status] === '已取消' ? 'info' : 'primary') as any" size="small" effect="light"> :data="list"
:loading="loading"
:total="total"
:page="page"
:limit="limit"
:show-pagination="total > 10"
:error="error"
empty-text="暂无订单"
borderless
row-key="id"
@page-change="onPageChange"
@limit-change="onLimitChange"
@selection-change="onSelectionChange"
>
<template #contractCode="{ row }">
<span v-if="row.contractCode">{{ row.contractCode }}</span>
<span v-else class="rr-muted">--</span>
</template>
<template #amount="{ row }">
<span class="rr-amount">¥ {{ row.totalAmount.toLocaleString() }}</span>
</template>
<template #status="{ row }">
<el-tag
:type="(STATUS_LABEL[row.status] === '已完成' ? 'success' : STATUS_LABEL[row.status] === '已取消' ? 'info' : 'primary') as any"
size="small"
effect="light"
>
{{ STATUS_LABEL[row.status] || `状态${row.status}` }} {{ STATUS_LABEL[row.status] || `状态${row.status}` }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> <template #action="{ row }">
<el-table-column label="下单时间" width="170"> <el-button text type="primary" size="small" @click="onViewDetail(row)">详情</el-button>
<template #default="{ row }">{{ row.createDate }}</template> <el-button v-if="row.status === 1" text type="primary" size="small" @click="onPay(row)">付款</el-button>
</el-table-column> <el-button v-if="row.status === 2" text type="primary" size="small" @click="onShip(row)">发货</el-button>
<el-table-column label="操作" width="240" align="center" fixed="right"> <el-button v-if="row.status === 3" text type="primary" size="small" @click="onConfirmReceipt(row)">收货</el-button>
<template #default="{ row }"> <el-button v-if="[1,2,3,4].includes(row.status)" text type="warning" size="small" @click="onRefund(row)">退款</el-button>
<el-button text type="primary" size="small" @click.stop="onViewDetail(row)">详情</el-button> <el-button v-if="[1,2].includes(row.status)" text type="danger" size="small" @click="onCancel(row)">取消</el-button>
<el-button v-if="row.status === 1" text type="primary" size="small" @click.stop="onPay(row)">付款</el-button> <el-button text size="small" @click="onPrint(row)">打印</el-button>
<el-button v-if="row.status === 2" text type="primary" size="small" @click.stop="onShip(row)">发货</el-button>
<el-button v-if="row.status === 3" text type="primary" size="small" @click.stop="onConfirmReceipt(row)">收货</el-button>
<el-button v-if="[1,2,3,4].includes(row.status)" text type="warning" size="small" @click.stop="onRefund(row)">退款</el-button>
<el-button v-if="[1,2].includes(row.status)" text type="danger" size="small" @click.stop="onCancel(row)">取消</el-button>
<el-button text size="small" @click.stop="onPrint(row)">打印</el-button>
</template> </template>
</el-table-column> </DataTable>
</el-table>
<div class="rr-pagination">
<el-pagination :current-page="page" :page-sizes="[10, 20, 50, 100]" :page-size="limit" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="onLimitChange" @current-change="onPageChange" />
</div>
</div> </div>
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.rr-page { max-width: 1500px; } .rr-page { max-width: 1500px; }
.rr-page-header { margin-bottom: 16px; display: flex; justify-content: space-between; align-items: flex-start; }
.rr-page-title { font-size: 22px; font-weight: 600; color: var(--ink); margin: 0 0 4px 0; }
.rr-page-sub { color: var(--ink-3); font-size: 13px; margin: 0; }
.rr-page-actions { display: flex; gap: 8px; }
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
.rr-search { padding: 16px 20px; }
.rr-tabs-row { margin-bottom: 16px; } .rr-tabs-row { margin-bottom: 16px; }
.rr-tabs { display: flex; gap: 4px; padding: 4px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); width: fit-content; flex-wrap: wrap; } .rr-tabs { display: flex; gap: 4px; padding: 4px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); width: fit-content; flex-wrap: wrap; }
.rr-tab { padding: 6px 16px; border-radius: var(--radius); font-size: 13px; color: var(--ink-2); cursor: pointer; } .rr-tab { padding: 6px 16px; border-radius: var(--radius); font-size: 13px; color: var(--ink-2); cursor: pointer; transition: all 0.15s; }
.rr-tab:hover { background: var(--bg-soft); }
.rr-tab.active { color: white; background: var(--primary); font-weight: 500; } .rr-tab.active { color: white; background: var(--primary); font-weight: 500; }
.rr-pagination { padding: 12px 20px; display: flex; justify-content: flex-end; border-top: 1px solid var(--border); } .rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
:deep(.rr-clickable) { cursor: pointer; } .rr-muted { color: var(--ink-3); }
:deep(.rr-clickable:hover > td) { background: var(--bg-soft) !important; } .rr-amount { font-weight: 600; color: var(--primary); }
</style> </style>