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:
parent
8601032bba
commit
894d381953
177
src/components/FormDialog/FormDialog.vue
Normal file
177
src/components/FormDialog/FormDialog.vue
Normal 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>
|
||||
65
src/components/PageHeader/PageHeader.vue
Normal file
65
src/components/PageHeader/PageHeader.vue
Normal 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>
|
||||
147
src/components/SearchBar/SearchBar.vue
Normal file
147
src/components/SearchBar/SearchBar.vue
Normal 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>
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 订单管理列表 - 完整版 (W5.x.x+5 整改)
|
||||
* F006 订单管理列表 - W9 A3 标准化示范
|
||||
*
|
||||
* 沿用老 vue order/order-good.vue 289 行 + order/serviceorder 154 行等:
|
||||
* - 搜索: 订单编号/合同编号/供应商/采购方/状态/创建时间范围
|
||||
@ -9,12 +9,19 @@
|
||||
* - 操作: 详情/付款/发货/收货/取消/退款/导出
|
||||
* - 批量导出
|
||||
* - 通过 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 { useRouter, useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||
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 {
|
||||
id: string
|
||||
@ -63,9 +70,17 @@ const FALLBACK: Order[] = [
|
||||
const list = ref<Order[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const page = ref(1)
|
||||
const limit = ref(10)
|
||||
const error = ref<string | null>(null)
|
||||
const filters = reactive({
|
||||
orderNo: '',
|
||||
contractCode: '',
|
||||
enterpriseName: '',
|
||||
status: '',
|
||||
startDate: '',
|
||||
endDate: ''
|
||||
})
|
||||
const selection = ref<Order[]>([])
|
||||
|
||||
const router = useRouter()
|
||||
@ -80,161 +95,168 @@ const listType = computed(() => {
|
||||
return 'good'
|
||||
})
|
||||
|
||||
const filters = reactive({
|
||||
orderNo: '',
|
||||
contractCode: '',
|
||||
enterpriseName: '',
|
||||
status: '',
|
||||
startDate: '',
|
||||
endDate: ''
|
||||
})
|
||||
|
||||
const typeLabel = computed(() => {
|
||||
const endpoint = computed(() => {
|
||||
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() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
let rows = [...FALLBACK]
|
||||
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)
|
||||
const r: any = await httpGet(endpoint.value, { page: page.value, limit: limit.value, ...filters })
|
||||
let rows: Order[] = (r?.data?.list || []).map((it: any) => ({ ...it, raw: it }))
|
||||
list.value = rows
|
||||
total.value = rows.length
|
||||
total.value = r?.data?.total || 0
|
||||
} catch (e: any) {
|
||||
error.value = e?.msg || e?.message || '加载失败'
|
||||
list.value = FALLBACK
|
||||
total.value = FALLBACK.length
|
||||
} finally { loading.value = false }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
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 onLimitChange(l: number) { limit.value = l; page.value = 1; load() }
|
||||
function onStatusTab(v: string) { filters.status = v; load() }
|
||||
function onLimitChange(l: number) { limit.value = l; load() }
|
||||
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 onPay(row: Order) { ElMessage.success(`付款 (mock) ${row.orderNo}`) }
|
||||
function onShip(row: Order) { ElMessage.success(`发货 (mock) ${row.orderNo}`) }
|
||||
function onConfirmReceipt(row: Order) { ElMessage.success(`确认收货 (mock) ${row.orderNo}`) }
|
||||
function onCancel(row: Order) { ElMessage.info(`取消 (mock) ${row.orderNo}`) }
|
||||
function onRefund(row: Order) { ElMessage.info(`退款 (mock) ${row.orderNo}`) }
|
||||
function onExport() { ElMessage.success('导出订单 (mock)') }
|
||||
function onExportSelected() { if (!selection.value.length) { ElMessage.warning('请先选择订单'); return } ElMessage.success(`导出 ${selection.value.length} 个订单 (mock)`) }
|
||||
function onPrint(row: Order) { ElMessage.info(`打印 (mock) ${row.orderNo}`) }
|
||||
function onPay(row: Order) { ElMessage.success(`付款 ${row.orderNo} (mock)`); load() }
|
||||
function onShip(row: Order) { ElMessage.success(`发货 ${row.orderNo} (mock)`); load() }
|
||||
function onConfirmReceipt(row: Order) { ElMessage.success(`确认收货 ${row.orderNo} (mock)`); load() }
|
||||
function onRefund(row: Order) { ElMessage.success(`退款 ${row.orderNo} (mock)`); load() }
|
||||
function onCancel(row: Order) { ElMessage.success(`取消 ${row.orderNo} (mock)`); load() }
|
||||
function onPrint(row: Order) { ElMessage.info(`打印 ${row.orderNo} (mock)`) }
|
||||
function onExportSelected() {
|
||||
if (!selection.value.length) { ElMessage.warning('请先选择订单'); return }
|
||||
ElMessage.success(`导出 ${selection.value.length} 个订单 (mock)`)
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rr-page">
|
||||
<div class="rr-page-header">
|
||||
<div>
|
||||
<h2 class="rr-page-title">{{ typeLabel }}</h2>
|
||||
<p class="rr-page-sub">共 {{ total }} 条订单</p>
|
||||
</div>
|
||||
<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>
|
||||
<PageHeader title="订单管理" :sub="`共 ${total} 条, 选中 ${selection.length}`">
|
||||
<el-button v-if="selection.length > 0" type="primary" @click="onExportSelected">
|
||||
导出选中 ({{ selection.length }})
|
||||
</el-button>
|
||||
</PageHeader>
|
||||
|
||||
<!-- 搜索区 (沿用老 vue: 订单号/合同号/供应商/状态/时间) -->
|
||||
<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 -->
|
||||
<!-- 状态 tab -->
|
||||
<div class="rr-tabs-row">
|
||||
<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>
|
||||
|
||||
<!-- 表格 -->
|
||||
<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">
|
||||
<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">
|
||||
<el-table-column type="selection" width="50" align="center" @selectable="(row: any) => row.status !== 7" />
|
||||
<el-table-column type="index" label="#" width="50" align="center" />
|
||||
<el-table-column prop="orderNo" label="订单号" width="180" />
|
||||
<el-table-column prop="contractCode" label="合同号" width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="enterpriseName" label="供应商" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="orderUserName" label="采购方" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="金额" width="120" align="right">
|
||||
<template #default="{ row }">¥ {{ row.totalAmount.toLocaleString() }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数量" prop="totalNum" width="80" align="center" />
|
||||
<el-table-column label="已发" prop="shippedNum" width="80" align="center" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ 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}` }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="下单时间" width="170">
|
||||
<template #default="{ row }">{{ row.createDate }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="240" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button text type="primary" size="small" @click.stop="onViewDetail(row)">详情</el-button>
|
||||
<el-button v-if="row.status === 1" text type="primary" size="small" @click.stop="onPay(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>
|
||||
</el-table-column>
|
||||
</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>
|
||||
<DataTable
|
||||
:columns="[
|
||||
{ type: 'selection', label: '', width: 50 },
|
||||
{ prop: 'orderNo', label: '订单号', width: 180 },
|
||||
{ prop: 'contractCode', label: '合同号', width: 160, slot: 'contractCode' },
|
||||
{ prop: 'enterpriseName', label: '供应商', minWidth: 180 },
|
||||
{ prop: 'orderUserName', label: '采购方', minWidth: 160 },
|
||||
{ prop: 'totalAmount', label: '金额', width: 130, align: 'right', slot: 'amount' },
|
||||
{ prop: 'totalNum', label: '数量', width: 80, align: 'center' },
|
||||
{ prop: 'shippedNum', label: '已发', width: 80, align: 'center' },
|
||||
{ prop: 'status', label: '状态', width: 110, align: 'center', slot: 'status' },
|
||||
{ prop: 'createDate', label: '下单时间', width: 170 },
|
||||
{ label: '操作', width: 280, align: 'center', slot: 'action' }
|
||||
]"
|
||||
: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}` }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<el-button text type="primary" size="small" @click="onViewDetail(row)">详情</el-button>
|
||||
<el-button v-if="row.status === 1" text type="primary" size="small" @click="onPay(row)">付款</el-button>
|
||||
<el-button v-if="row.status === 2" text type="primary" size="small" @click="onShip(row)">发货</el-button>
|
||||
<el-button v-if="row.status === 3" text type="primary" size="small" @click="onConfirmReceipt(row)">收货</el-button>
|
||||
<el-button v-if="[1,2,3,4].includes(row.status)" text type="warning" size="small" @click="onRefund(row)">退款</el-button>
|
||||
<el-button v-if="[1,2].includes(row.status)" text type="danger" size="small" @click="onCancel(row)">取消</el-button>
|
||||
<el-button text size="small" @click="onPrint(row)">打印</el-button>
|
||||
</template>
|
||||
</DataTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.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 { 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-pagination { padding: 12px 20px; display: flex; justify-content: flex-end; border-top: 1px solid var(--border); }
|
||||
:deep(.rr-clickable) { cursor: pointer; }
|
||||
:deep(.rr-clickable:hover > td) { background: var(--bg-soft) !important; }
|
||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||
.rr-muted { color: var(--ink-3); }
|
||||
.rr-amount { font-weight: 600; color: var(--primary); }
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user