Explorar o código

feat(workOrder): 新增销售工单管理模块

ylong hai 1 mes
pai
achega
b9ef70e013

+ 236 - 0
docs/TRAE_CODING_GUIDELINES.md

@@ -0,0 +1,236 @@
+# 书嗨后台管理系统 - Trae AI 代码编写规范与指南
+
+本文档旨在为 Trae AI(及开发者)提供统一的代码编写规范和最佳实践指南,确保代码风格一致、维护性高且符合项目架构要求。
+
+## 1. 技术栈与环境
+*   **核心框架**: Vue 3 (Composition API)
+*   **构建工具**: Vite
+*   **UI 组件库**: Element Plus + Ele Admin Plus (项目封装库)
+*   **CSS 框架**: Tailwind CSS (优先使用) + SCSS
+*   **语言**: JavaScript (部分 TypeScript 支持)
+
+## 2. 目录结构规范
+
+```text
+src/
+├── api/                # API 接口定义,按模块分类
+├── components/         # 通用组件
+│   ├── CommonPage/     # 列表页核心组件 (CommonTable, ProSearch 等)
+│   ├── ProForm/        # 高级表单组件
+│   └── ...
+├── utils/              # 工具函数
+│   ├── request.js      # Axios 封装 (核心)
+│   ├── use-dict-data.js # 字典 Hook
+│   └── ...
+├── views/              # 页面视图,按模块划分
+│   ├── finance/        # 财务模块
+│   │   ├── withdrawal/ # 提现管理
+│   │   │   ├── index.vue           # 列表主页
+│   │   │   ├── components/         # 页面独有组件
+│   │   │   │   ├── page-search.vue # 搜索栏
+│   │   │   │   ├── audit-dialog.vue# 审核弹窗
+│   │   │   │   └── ...
+│   └── ...
+```
+
+## 3. 页面开发模式 (Page Patterns)
+
+### 3.1 列表管理页 (List Page)
+所有“增删改查”类列表页面**必须**遵循以下结构:
+
+1.  **根容器**: 使用 `<ele-page flex-table>` 实现高度自适应。
+2.  **搜索区**: 引入独立的 `page-search.vue` 组件。
+3.  **表格区**: 使用 `<common-table>` 组件。
+
+**`index.vue` 模板:**
+
+```vue
+<template>
+    <ele-page flex-table>
+        <!-- 搜索栏 -->
+        <page-search @search="reload" />
+
+        <!-- 通用表格 -->
+        <common-table ref="pageRef" :pageConfig="pageConfig" :columns="columns">
+            <!-- 顶部工具栏 -->
+            <template #toolbar>
+                <div class="flex items-center mb-4">
+                    <el-button type="primary" @click="handleAdd" v-permission="'module:add'">新增</el-button>
+                    <!-- 统计信息等 -->
+                </div>
+            </template>
+            
+            <!-- 自定义列渲染 -->
+            <template #status="{ row }">
+                 <el-tag>{{ getStatusLabel(row.status) }}</el-tag>
+            </template>
+            
+            <!-- 操作列 -->
+            <template #action="{ row }">
+                <el-button link type="primary" @click="handleEdit(row)" v-permission="'module:edit'">编辑</el-button>
+            </template>
+        </common-table>
+
+        <!-- 弹窗组件 -->
+        <edit-dialog ref="editDialogRef" @success="reload" />
+    </ele-page>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import CommonTable from '@/components/CommonPage/CommonTable.vue';
+import PageSearch from './components/page-search.vue';
+import EditDialog from './components/edit-dialog.vue';
+
+// 页面配置
+const pageConfig = reactive({
+    pageUrl: '/sys/module/list', // 列表接口 URL
+    fileName: '导出文件名',
+    rowKey: 'id'
+});
+
+// 列定义
+const columns = ref([
+    { label: '名称', prop: 'name', align: 'center' },
+    { label: '状态', prop: 'status', slot: 'status', align: 'center' },
+    { columnKey: 'action', label: '操作', slot: 'action', fixed: 'right', width: 140 }
+]);
+
+const pageRef = ref(null);
+const editDialogRef = ref(null);
+
+const reload = (where) => pageRef.value?.reload(where);
+const handleAdd = () => editDialogRef.value?.handleOpen();
+const handleEdit = (row) => editDialogRef.value?.handleOpen(row);
+</script>
+```
+
+### 3.2 搜索组件 (Search Component)
+文件路径: `./components/page-search.vue`
+
+```vue
+<template>
+    <ele-card :body-style="{ paddingBottom: '8px' }">
+        <ProSearch :items="formItems" @search="search" />
+    </ele-card>
+</template>
+
+<script setup>
+import { reactive } from 'vue';
+import ProSearch from '@/components/CommonPage/ProSearch2.vue';
+
+const emit = defineEmits(['search']);
+
+const formItems = reactive([
+    { type: 'input', label: '关键词', prop: 'keywords' },
+    { 
+        type: 'dictSelect', 
+        label: '状态', 
+        prop: 'status', 
+        props: { code: 'sys_status' } // 对应字典编码
+    }
+]);
+
+const search = (data) => emit('search', data);
+</script>
+```
+
+### 3.3 弹窗组件 (Dialog Component)
+文件路径: `./components/*-dialog.vue`
+
+```vue
+<template>
+    <ele-modal v-model="visible" :title="title" width="600px">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+            <el-form-item label="名称" prop="name">
+                <el-input v-model="form.name" />
+            </el-form-item>
+        </el-form>
+        <template #footer>
+            <el-button @click="visible = false">取消</el-button>
+            <el-button type="primary" @click="handleSubmit">确定</el-button>
+        </template>
+    </ele-modal>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import request from '@/utils/request';
+import { ElMessage } from 'element-plus';
+
+const emit = defineEmits(['success']);
+const visible = ref(false);
+const title = ref('新增');
+const formRef = ref();
+const form = reactive({ id: null, name: '' });
+const rules = { name: [{ required: true, message: '请输入名称' }] };
+
+const handleOpen = (row) => {
+    if (row) {
+        title.value = '编辑';
+        Object.assign(form, row);
+    } else {
+        title.value = '新增';
+        // 重置表单逻辑
+        form.id = null;
+        form.name = '';
+    }
+    visible.value = true;
+};
+
+const handleSubmit = async () => {
+    await formRef.value.validate();
+    const url = form.id ? '/sys/module/update' : '/sys/module/add';
+    const res = await request.post(url, form);
+    if (res.data.code === 200) {
+        ElMessage.success(res.data.msg);
+        visible.value = false;
+        emit('success');
+    }
+};
+
+defineExpose({ handleOpen });
+</script>
+```
+
+## 4. 编码规范
+
+### 4.1 样式 (CSS/SCSS)
+*   **Tailwind CSS**: 优先使用 Tailwind 类名处理布局和间距。
+    *   `flex`, `items-center`, `justify-between`
+    *   `m-4` (margin), `p-4` (padding)
+    *   `w-full`, `h-full`
+*   **Element Plus 样式覆盖**: 尽量通过 props 控制,必要时在 `<style scoped>` 中使用 `:deep()`。
+
+### 4.2 脚本 (Script)
+*   使用 `<script setup>` 语法。
+*   **导入顺序**: Vue 核心 -> 第三方库 -> 项目组件 -> 工具函数/API。
+*   **字典数据**: 必须使用 `useDictData` Hook 获取字典数据。
+    ```js
+    import { useDictData } from '@/utils/use-dict-data';
+    const [statusOptions] = useDictData(['sys_status']);
+    ```
+
+### 4.3 网络请求
+*   使用 `@/utils/request` 实例。
+*   必须处理 `res.data.code === 200` 的业务成功逻辑。
+*   错误处理通常由拦截器统一处理,但在特定业务场景下可单独 `catch`。
+
+## 5. Trae AI 执行清单
+当用户请求创建新页面或功能时,请按以下步骤执行:
+
+1.  **需求分析**: 确认页面功能(列表/表单)、字段定义、API 接口。
+2.  **文件创建**:
+    *   `src/api/module/index.js` (如需)
+    *   `src/views/module/page/index.vue`
+    *   `src/views/module/page/components/page-search.vue`
+    *   `src/views/module/page/components/*-dialog.vue`
+3.  **代码实现**:
+    *   套用上述“页面开发模式”模板。
+    *   确保所有文本标签、字典编码与业务匹配。
+    *   添加必要的权限指令 (`v-permission`)。
+4.  **路由配置**: 检查并提示用户更新路由(如果是动态路由通常由后端控制,如果是静态路由需修改 `router/routes.js`)。
+5.  **预览验证**: 使用 `open_preview` 工具检查 UI 布局。
+
+---
+**注意**: 在修改现有代码时,请先读取原文件理解上下文,避免破坏现有逻辑。

+ 44 - 0
src/api/workOrder/index.js

@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 分页查询工单列表
+export function getWorkOrderPageList(query) {
+  return request({
+    url: '/workorder/workorderinfo/pagelist',
+    method: 'get',
+    params: query
+  })
+}
+
+// 新增工单
+export function addWorkOrder(data) {
+  return request({
+    url: '/workorder/workorderinfo/add',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改工单
+export function updateWorkOrder(data) {
+  return request({
+    url: '/workorder/workorderinfo/update',
+    method: 'post',
+    data: data
+  })
+}
+
+// 获取工单详细信息
+export function getWorkOrder(id) {
+  return request({
+    url: '/workorder/workorderinfo/' + id,
+    method: 'get'
+  })
+}
+
+// 删除工单
+export function delWorkOrder(id) {
+  return request({
+    url: '/workorder/workorderinfo/' + id,
+    method: 'delete'
+  })
+}

+ 182 - 0
src/views/workOrder/recycle/index.vue

@@ -0,0 +1,182 @@
+<template>
+    <ele-page flex-table>
+        <!-- Search Component -->
+        <PageSearch @search="handleSearch" :myPendingCount="myPendingCount" />
+
+        <!-- Common Table -->
+        <common-table ref="tableRef" :pageConfig="pageConfig" :columns="columns">
+            <!-- Toolbar -->
+            <template #toolbar>
+                <div class="flex items-center space-x-2">
+                    <el-button type="primary" @click="handleAdd" v-permission="'workOrder:sell:add'"
+                        :icon="Plus">新增工单</el-button>
+                    <el-button type="success" @click="handleBatchComplete" v-permission="'workOrder:sell:complete'"
+                        :icon="Check">批量完成</el-button>
+                </div>
+            </template>
+
+            <template #handleUsers="{ row }">
+                <div v-if="row.handleUsers && row.handleUsers.length">
+                    <span class="mr-1 text-gray-500">({{ getCompletedCount(row.handleUsers) }}/{{ row.handleUsers.length
+                        }})</span>
+                    <span v-for="(user, index) in row.handleUsers" :key="user.id">
+                        <span :class="{ 'text-green-600 font-medium': user.status === 2 }">{{ user.userName }}</span>
+                        <span v-if="user.status === 2" class="text-green-600 ml-0.5">✓</span>
+                        <span v-if="index < row.handleUsers.length - 1" class="text-gray-400">, </span>
+                    </span>
+                </div>
+                <div v-else class="text-gray-400">-</div>
+            </template>
+
+            <!-- Action Column -->
+            <template #action="{ row }">
+                <div class="flex justify-center space-x-2">
+                    <!-- Logic: Creator -->
+                    <template v-if="isCreator(row)">
+                        <template v-if="[1, 2].includes(row.status)"> <!-- Pending, Processing -->
+                            <el-button link type="danger" @click="handleVoid(row)">作废</el-button>
+                            <el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
+                            <el-button link type="success" @click="handleComplete(row)">完成</el-button>
+                        </template>
+                        <template v-else-if="[3, 4].includes(row.status)"> <!-- Completed, Void -->
+                            <el-button link type="warning" @click="handleReopen(row)">重开</el-button>
+                        </template>
+                    </template>
+                    <!-- Logic: Assignee -->
+                    <template v-else-if="isAssignee(row)">
+                        <template v-if="[1, 2].includes(row.status)">
+                            <el-button link type="success" @click="handleComplete(row)">完成</el-button>
+                            <el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
+                        </template>
+                    </template>
+                </div>
+            </template>
+        </common-table>
+
+        <!-- Edit Dialog -->
+        <EditDialog ref="editDialogRef" @success="refreshPage" />
+    </ele-page>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import CommonTable from '@/components/CommonPage/CommonTable.vue'
+import PageSearch from '@/views/workOrder/sell/components/page-search.vue'
+import EditDialog from '@/views/workOrder/sell/components/edit-dialog.vue'
+import { useUserStore } from '@/store/modules/user'
+import { Plus, Check } from '@element-plus/icons-vue'
+
+const userStore = useUserStore()
+const currentUserId = computed(() => userStore.info?.userId || userStore.info?.id)
+
+const tableRef = ref(null)
+const editDialogRef = ref(null)
+const myPendingCount = ref(0) // Should be fetched separately if needed
+
+// Page Config for CommonTable
+const pageConfig = reactive({
+    pageUrl: '/workorder/workorderinfo/pagelist',
+    rowKey: 'id',
+    fileName: '工单列表-回收',
+    params: {
+        type: 2,
+    }
+})
+
+// Column Definitions
+const columns = ref([
+    { type: 'selection', width: 50, align: 'center' },
+    { prop: 'id', label: '工单任务编号', width: 120, align: 'center' },
+    { prop: 'taskTypeName', label: '任务类型', width: 120, align: 'center' },
+    { prop: 'inspectionStatusName', label: '验货状态', width: 100, align: 'center' },
+    { prop: 'statusName', label: '状态', width: 100, align: 'center' },
+    { prop: 'handleUsers', label: '指派给', minWidth: 150, slot: 'handleUsers' },
+    { prop: 'deatil', label: '任务详情', minWidth: 200, showOverflowTooltip: true },
+    { prop: 'createTime', label: '创建时间', width: 160, align: 'center' },
+    { prop: 'finishTime', label: '完成时间', width: 160, align: 'center' },
+    { prop: 'createUserName', label: '创建人', width: 100, align: 'center' },
+    { columnKey: 'action', label: '操作', width: 150, fixed: 'right', align: 'center', slot: 'action' }
+])
+
+// Helpers
+const getTaskTypeName = (type) => {
+    const map = { 1: '仓库缺货', 2: '部分发货', 3: '书单不符' }
+    return map[type] || '未知'
+}
+
+const getStatusName = (status) => {
+    const map = { 1: '待处理', 2: '处理中', 3: '已完成', 4: '已作废' }
+    return map[status] || '未知'
+}
+
+const getStatusType = (status) => {
+    const map = { 1: 'primary', 2: 'warning', 3: 'success', 4: 'info' }
+    return map[status] || 'info'
+}
+
+const getCompletedCount = (handleUsers) => {
+    if (!handleUsers) return 0
+    return handleUsers.filter(u => u.status === 2).length
+}
+
+const isCreator = (row) => {
+    if (!currentUserId.value) return false
+    return row.createUserId === currentUserId.value
+}
+
+const isAssignee = (row) => {
+    if (!currentUserId.value) return false
+    if (!row.handleUsers) return false
+    return row.handleUsers.some(u => u.userId === currentUserId.value)
+}
+
+// Event Handlers
+const handleSearch = (params) => {
+    tableRef.value?.reload(params)
+}
+
+const handleAdd = () => {
+    editDialogRef.value?.handleOpen()
+}
+
+const handleBatchComplete = () => {
+    const selectedRows = tableRef.value?.getSelections()
+
+    tableRef.value?.operatBatch({
+        method: 'post',
+        url: '/workorder/workorderinfo/finish',
+        title: `确认批量完成这 ${selectedRows.length} 个工单吗?`,
+        data: { ids: selectedRows.map(row => row.id) }
+    })
+}
+
+const handleEdit = (row) => {
+    editDialogRef.value?.handleOpen(row)
+}
+
+const refreshPage = () => {
+    tableRef.value?.reload()
+}
+
+const handleVoid = (row) => {
+    tableRef.value?.messageBoxConfirm({
+        message: '确认作废该工单吗?',
+        fetch: () => proxy.$http.post('/workorder/workorderinfo/cancel', { id: row.id })
+    })
+}
+
+const handleComplete = (row) => {
+    tableRef.value?.messageBoxConfirm({
+        message: '确认完成该工单吗?',
+        fetch: () => proxy.$http.post('/workorder/workorderinfo/finish', { ids: [row.id] })
+    })
+}
+
+const handleReopen = (row) => {
+    tableRef.value?.messageBoxConfirm({
+        message: '确认重开该工单吗?',
+        fetch: () => proxy.$http.post('/workorder/workorderinfo/reopen', { id: row.id })
+    })
+}
+</script>

+ 313 - 0
src/views/workOrder/sell/components/edit-dialog.vue

@@ -0,0 +1,313 @@
+<template>
+    <ele-modal :title="title" v-model="visible" width="1100px" @closed="handleClose" :close-on-click-modal="false"
+        destroy-on-close :body-style="{ padding: '20px' }">
+        <el-row :gutter="20">
+            <!-- Left Column: Form -->
+            <el-col :span="14" class="border-r border-gray-200 pr-5">
+                <ProForm ref="proFormRef" :model="form" :rules="rules" :items="formItems" label-width="100px"
+                    :footer="false" @updateValue="(prop, val) => form[prop] = val">
+                    <template #imageUpload="{ proForm }">
+                        <ImageUpload v-model="proForm.model.fileList" :limit="5" />
+                    </template>
+                </ProForm>
+            </el-col>
+
+            <!-- Right Column: Info & Timeline -->
+            <el-col :span="10" class="pl-2">
+                <TrackingTimeline :logs="logs" />
+            </el-col>
+        </el-row>
+
+        <template #footer>
+            <div class="flex justify-center space-x-4">
+                <el-button type="warning" plain @click="handleFollowUp" v-if="form.id">待跟进</el-button>
+                <el-button type="success" plain @click="handleFeedback" v-if="form.id">反馈</el-button>
+                <el-button @click="handleClose">取消</el-button>
+                <el-button type="primary" @click="handleSubmit" :loading="loading">保存</el-button>
+            </div>
+        </template>
+    </ele-modal>
+</template>
+
+<script setup>
+import { ref, reactive, computed, watch, getCurrentInstance } from 'vue';
+import { ElMessage } from 'element-plus';
+import ImageUpload from '@/components/ImageUpload/index.vue';
+import ProForm from '@/components/ProForm/index.vue';
+import TrackingTimeline from './tracking-timeline.vue';
+import { pageUsers } from '@/api/system/user';
+
+const { proxy } = getCurrentInstance();
+
+const emit = defineEmits(['submit', 'success']);
+
+const visible = ref(false);
+const title = ref('工单信息');
+const loading = ref(false);
+// Use proFormRef instead of formRef
+const proFormRef = ref(null);
+
+const form = reactive({
+    id: undefined,
+    taskType: undefined,
+    handleUserId: [], // Changed from undefined to []
+    detail: '',
+    fileList: '',
+    carrierId: undefined,
+    waybillCode: '',
+    inspectionStatus: undefined,
+    orderNo: '',
+    type: 1,// 1-卖书 2-收书
+});
+
+const logs = ref([]);
+const userList = ref([]);
+const userLoading = ref(false);
+
+const rules = {
+    taskType: [
+        { required: true, message: '请选择任务类型', trigger: 'change' }
+    ],
+    handleUserId: [
+        { required: true, message: '请选择指派人', trigger: 'change' }
+    ],
+    detail: [{ required: true, message: '请输入详情', trigger: 'blur' }],
+    inspectionStatus: [
+        { required: true, message: '请选择验货状态', trigger: 'change' }
+    ]
+};
+
+// Form Items Configuration
+const formItems = computed(() => [
+    {
+        label: '任务类型',
+        prop: 'taskType',
+        type: 'dictSelect',
+        props: { class: 'w-full', placeholder: '请选择任务类型', code: 'task_type' }
+    },
+    {
+        label: '指派给',
+        prop: 'handleUserId',
+        type: 'select',
+        options: userList.value.map(user => ({
+            label: user.nickName || user.userName,
+            value: user.userId
+        })),
+        props: {
+            class: 'w-full',
+            placeholder: '请选择',
+            filterable: true,
+            remote: true,
+            multiple: true, // Enable multiple selection
+            remoteMethod: searchUsers,
+            loading: userLoading.value
+        }
+    },
+    {
+        label: '任务详情',
+        prop: 'detail',
+        type: 'textarea',
+        props: { rows: 4, placeholder: '请输入' }
+    },
+    {
+        label: '图片/附件',
+        prop: 'fileList',
+        type: 'imageUpload',
+        itemType: 'view' // Triggers the slot mechanism
+    },
+    {
+        label: '承运商',
+        prop: 'carrierId',
+        type: 'dictSelect',
+        props: {
+            class: 'w-full',
+            placeholder: '请选择发货快递',
+            filterable: true,
+            code: 'final_express'
+        }
+    },
+    {
+        label: '发出快递单号',
+        prop: 'waybillCode',
+        type: 'input',
+        props: { placeholder: '请输入' }
+    },
+    {
+        label: '验货状态',
+        prop: 'inspectionStatus',
+        type: 'dictSelect',
+        props: { class: 'w-full', placeholder: '请选择验货状态', code: 'inspection_status' }
+    },
+    {
+        label: '订单号',
+        prop: 'orderNo',
+        type: 'input',
+        props: { placeholder: '请输入' }
+    }
+]);
+
+// 搜索用户
+const searchUsers = async (query = '') => {
+    userLoading.value = true;
+    try {
+        const res = await pageUsers({
+            pageNum: 1,
+            pageSize: 50,
+            nickName: query
+        });
+        if (res.code === 200) {
+            userList.value = res.rows || res.data;
+        }
+    } catch (e) {
+        console.error(e);
+    } finally {
+        userLoading.value = false;
+    }
+};
+
+const handleOpen = async (data = {}) => {
+    visible.value = true;
+    title.value = data.id ? '编辑工单' : '新增工单';
+
+    // Initialize lists
+    if (userList.value.length === 0) searchUsers();
+
+    // Reset form
+    Object.assign(form, {
+        id: undefined,
+        taskType: undefined,
+        handleUserId: [], // Reset to empty array
+        detail: '',
+        fileList: '',
+        carrierId: undefined,
+        waybillCode: '',
+        inspectionStatus: undefined,
+        orderNo: '',
+        type: 1,// 1-卖书 2-收书
+    });
+    logs.value = [];
+
+    if (data.id) {
+        loading.value = true;
+        try {
+            // Fetch full details
+            const res = await proxy.$http.get('/workorder/workorderinfo/getInfo/' + data.id);
+            if (res.data && res.data.code === 200) {
+                const detail = res.data.data;
+
+                // Map backend data to form
+                form.id = detail.id;
+                form.taskType = String(detail.taskType); // Convert to number
+                form.inspectionStatus = String(detail.inspectionStatus); // Convert to number
+                form.waybillCode = detail.waybillCode;
+
+                // Map mismatched fields
+                form.detail = detail.deatil; // deatil -> detail
+                form.orderNo = detail.orderId; // orderId -> orderNo
+                form.carrierId = detail.expressType ? String(detail.expressType) : undefined; // expressType -> carrierId (String for dict match)
+
+                // handleUsers array of objects -> array of IDs
+                if (detail.handleUsers && detail.handleUsers.length > 0) {
+                    form.handleUserId = detail.handleUsers.map(user => {
+                        // Ensure user is in list for display
+                        if (typeof user === 'object' && user.userId) {
+                            if (!userList.value.find(u => u.userId === user.userId)) {
+                                userList.value.push(user);
+                            }
+                            return user.userId;
+                        }
+                        return user;
+                    });
+                } else {
+                    form.handleUserId = [];
+                }
+
+                // imgInfo object -> fileList JSON string
+                if (detail.imgInfo && detail.imgInfo.imgUrlList && Array.isArray(detail.imgInfo.imgUrlList)) {
+                    form.fileList = JSON.stringify(detail.imgInfo.imgUrlList);
+                } else {
+                    form.fileList = '';
+                }
+
+                // Logs
+                if (detail.changeRecords) {
+                    logs.value = detail.changeRecords.map(item => ({
+                        createTime: item.createTime,
+                        createName: item.createUserName,
+                        content: item.remark
+                    }));
+                } else {
+                    logs.value = [];
+                }
+            } else {
+                // Fallback mapping if API fails (unlikely to work well but safe)
+                Object.assign(form, data);
+            }
+        } catch (e) {
+            console.error(e);
+            Object.assign(form, data);
+        } finally {
+            loading.value = false;
+        }
+    }
+};
+
+const handleClose = () => {
+    visible.value = false;
+    // Use proFormRef to reset
+    proFormRef.value?.resetFields();
+};
+
+const handleSubmit = () => {
+    // Use proFormRef to validate
+    proFormRef.value?.validate((valid) => {
+        if (valid) {
+            loading.value = true;
+
+            // Map form data to API structure
+            const submitData = {
+                id: form.id,
+                taskType: form.taskType,
+                inspectionStatus: form.inspectionStatus,
+                waybillCode: form.waybillCode,
+
+                deatil: form.detail, // detail -> deatil
+                orderId: form.orderNo, // orderNo -> orderId
+                expressType: form.carrierId, // carrierId -> expressType
+                type: 1, // Fixed: 1-Sell Book
+
+                // handleUserId is already an array of IDs
+                handleUsers: Array.isArray(form.handleUserId) ? form.handleUserId : (form.handleUserId ? [form.handleUserId] : []),
+
+                // fileList string -> imgInfo array
+                imgInfo: form.fileList ? JSON.parse(form.fileList) : []
+            };
+
+            const url = form.id ? '/workorder/workorderinfo/update' : '/workorder/workorderinfo/add';
+            proxy.$http.post(url, submitData).then(res => {
+                if (res.data.code === 200) {
+                    ElMessage.success(form.id ? '修改成功' : '新增成功');
+                    emit('success');
+                    handleClose();
+                } else {
+                    ElMessage.error(res.data.msg || '操作失败');
+                }
+            }).catch(e => {
+                console.error(e);
+            }).finally(() => {
+                loading.value = false;
+            });
+        }
+    });
+};
+
+const handleFollowUp = () => {
+    ElMessage.info('功能待开发: 待跟进');
+};
+
+const handleFeedback = () => {
+    ElMessage.info('功能待开发: 反馈');
+};
+
+defineExpose({ handleOpen });
+</script>

+ 89 - 0
src/views/workOrder/sell/components/page-search.vue

@@ -0,0 +1,89 @@
+<template>
+	<ele-card :body-style="{ paddingBottom: '8px' }">
+		<ProSearch ref="proSearchRef" :items="formItems" @search="search" />
+
+		<!-- Quick Actions -->
+		<div class="flex items-center space-x-4 mt-2 px-4 border-t pt-2 border-gray-100">
+			<div class="flex items-center cursor-pointer select-none" @click="toggleMyPending">
+				<span class="text-gray-600 font-medium mr-2">快捷查询:</span>
+				<span class="px-3 py-1 rounded-full text-sm transition-colors duration-200"
+					:class="myHandleWorkOrder == 1 ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 text-gray-500 hover:bg-gray-200'">
+					待我完成
+					<span v-if="myPendingCount > 0" class="ml-1 font-bold text-red-500">({{ myPendingCount }})</span>
+				</span>
+			</div>
+		</div>
+	</ele-card>
+</template>
+
+<script setup>
+import { reactive, defineEmits, defineProps, ref } from 'vue';
+import ProSearch from '@/components/CommonPage/ProSearch2.vue';
+
+const props = defineProps({
+	myPendingCount: {
+		type: Number,
+		default: 0
+	}
+});
+
+const emit = defineEmits(['search']);
+
+const proSearchRef = ref(null);
+const myHandleWorkOrder = ref(0);
+
+const formItems = reactive([
+	{
+		type: 'dictSelect',
+		label: '任务状态',
+		prop: 'status',
+		props: { code: "work_order_status" },
+		colProps: { span: 3 }
+	},
+	{
+		type: 'dictSelect',
+		label: '任务类型',
+		prop: 'taskType',
+		props: { code: "task_type" },
+		colProps: { span: 3 }
+	},
+	{
+		type: 'dictSelect',
+		label: '验货状态',
+		prop: 'inspectionStatus',
+		props: { code: "inspection_status" },
+		colProps: { span: 3 }
+	},
+	{ type: 'input', label: '运单号', prop: 'waybillCode', placeholder: '请输入运单号' },
+	{
+		type: 'datetimerange',
+		label: '创建时间',
+		prop: 'createTime',
+		colProps: { span: 6 } // Make date range wider
+	},
+]);
+
+const toggleMyPending = () => {
+	myHandleWorkOrder.value = myHandleWorkOrder.value === 0 ? 1 : 0;
+	// Trigger search in ProSearch to get current form data
+	if (proSearchRef.value) {
+		proSearchRef.value.search();
+	}
+};
+
+const search = (data) => {
+	const params = { ...data };
+
+	// Handle date range
+	if (params.createTime && Array.isArray(params.createTime) && params.createTime.length === 2) {
+		params.createTimeStart = params.createTime[0];
+		params.createTimeEnd = params.createTime[1];
+		delete params.createTime;
+	}
+
+	// Add quick action state
+	params.myHandleWorkOrder = myHandleWorkOrder.value;
+
+	emit('search', params);
+};
+</script>

+ 33 - 0
src/views/workOrder/sell/components/tracking-timeline.vue

@@ -0,0 +1,33 @@
+<template>
+    <div class="time-line">
+        <div class="border-l-4 border-green-500 pl-2 font-medium mb-4 text-gray-700 ml-1">
+            任务跟踪记录
+        </div>
+        <div v-if="logs.length > 0" style="max-height: 500px; overflow: auto;">
+            <el-timeline>
+                <el-timeline-item v-for="(log, index) in logs" :key="index" :timestamp="log.createTime" placement="top">
+                    <div class="text-sm flex">
+                        <div class="font-medium text-gray-700">
+                            {{ log.createName }}:
+                        </div>
+                        <div class="text-gray-500">
+                            {{ log.content }}
+                        </div>
+                    </div>
+                </el-timeline-item>
+            </el-timeline>
+        </div>
+        <div v-else class="text-gray-400 text-sm py-4">
+            暂无记录
+        </div>
+    </div>
+</template>
+
+<script setup>
+defineProps({
+    logs: {
+        type: Array,
+        default: () => []
+    }
+});
+</script>

+ 182 - 0
src/views/workOrder/sell/index.vue

@@ -0,0 +1,182 @@
+<template>
+    <ele-page flex-table>
+        <!-- Search Component -->
+        <PageSearch @search="handleSearch" :myPendingCount="myPendingCount" />
+
+        <!-- Common Table -->
+        <common-table ref="tableRef" :pageConfig="pageConfig" :columns="columns">
+            <!-- Toolbar -->
+            <template #toolbar>
+                <div class="flex items-center space-x-2">
+                    <el-button type="primary" @click="handleAdd" v-permission="'workOrder:sell:add'"
+                        :icon="Plus">新增工单</el-button>
+                    <el-button type="success" @click="handleBatchComplete" v-permission="'workOrder:sell:complete'"
+                        :icon="Check">批量完成</el-button>
+                </div>
+            </template>
+
+            <template #handleUsers="{ row }">
+                <div v-if="row.handleUsers && row.handleUsers.length">
+                    <span class="mr-1 text-gray-500">({{ getCompletedCount(row.handleUsers) }}/{{ row.handleUsers.length
+                    }})</span>
+                    <span v-for="(user, index) in row.handleUsers" :key="user.id">
+                        <span :class="{ 'text-green-600 font-medium': user.status === 2 }">{{ user.userName }}</span>
+                        <span v-if="user.status === 2" class="text-green-600 ml-0.5">✓</span>
+                        <span v-if="index < row.handleUsers.length - 1" class="text-gray-400">, </span>
+                    </span>
+                </div>
+                <div v-else class="text-gray-400">-</div>
+            </template>
+
+            <!-- Action Column -->
+            <template #action="{ row }">
+                <div class="flex justify-center space-x-2">
+                    <!-- Logic: Creator -->
+                    <template v-if="isCreator(row)">
+                        <template v-if="[1, 2].includes(row.status)"> <!-- Pending, Processing -->
+                            <el-button link type="danger" @click="handleVoid(row)">作废</el-button>
+                            <el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
+                            <el-button link type="success" @click="handleComplete(row)">完成</el-button>
+                        </template>
+                        <template v-else-if="[3, 4].includes(row.status)"> <!-- Completed, Void -->
+                            <el-button link type="warning" @click="handleReopen(row)">重开</el-button>
+                        </template>
+                    </template>
+                    <!-- Logic: Assignee -->
+                    <template v-else-if="isAssignee(row)">
+                        <template v-if="[1, 2].includes(row.status)">
+                            <el-button link type="success" @click="handleComplete(row)">完成</el-button>
+                            <el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
+                        </template>
+                    </template>
+                </div>
+            </template>
+        </common-table>
+
+        <!-- Edit Dialog -->
+        <EditDialog ref="editDialogRef" @success="refreshPage" />
+    </ele-page>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import CommonTable from '@/components/CommonPage/CommonTable.vue'
+import PageSearch from './components/page-search.vue'
+import EditDialog from './components/edit-dialog.vue'
+import { useUserStore } from '@/store/modules/user'
+import { Plus, Check } from '@element-plus/icons-vue'
+
+const userStore = useUserStore()
+const currentUserId = computed(() => userStore.info?.userId || userStore.info?.id)
+
+const tableRef = ref(null)
+const editDialogRef = ref(null)
+const myPendingCount = ref(0) // Should be fetched separately if needed
+
+// Page Config for CommonTable
+const pageConfig = reactive({
+    pageUrl: '/workorder/workorderinfo/pagelist',
+    rowKey: 'id',
+    fileName: '工单列表-卖书',
+    params: {
+        type: 1,
+    }
+})
+
+// Column Definitions
+const columns = ref([
+    { type: 'selection', width: 50, align: 'center' },
+    { prop: 'id', label: '工单任务编号', width: 120, align: 'center' },
+    { prop: 'taskTypeName', label: '任务类型', width: 120, align: 'center' },
+    { prop: 'inspectionStatusName', label: '验货状态', width: 100, align: 'center' },
+    { prop: 'statusName', label: '状态', width: 100, align: 'center' },
+    { prop: 'handleUsers', label: '指派给', minWidth: 150, slot: 'handleUsers' },
+    { prop: 'deatil', label: '任务详情', minWidth: 200, showOverflowTooltip: true },
+    { prop: 'createTime', label: '创建时间', width: 160, align: 'center' },
+    { prop: 'finishTime', label: '完成时间', width: 160, align: 'center' },
+    { prop: 'createUserName', label: '创建人', width: 100, align: 'center' },
+    { columnKey: 'action', label: '操作', width: 150, fixed: 'right', align: 'center', slot: 'action' }
+])
+
+// Helpers
+const getTaskTypeName = (type) => {
+    const map = { 1: '仓库缺货', 2: '部分发货', 3: '书单不符' }
+    return map[type] || '未知'
+}
+
+const getStatusName = (status) => {
+    const map = { 1: '待处理', 2: '处理中', 3: '已完成', 4: '已作废' }
+    return map[status] || '未知'
+}
+
+const getStatusType = (status) => {
+    const map = { 1: 'primary', 2: 'warning', 3: 'success', 4: 'info' }
+    return map[status] || 'info'
+}
+
+const getCompletedCount = (handleUsers) => {
+    if (!handleUsers) return 0
+    return handleUsers.filter(u => u.status === 2).length
+}
+
+const isCreator = (row) => {
+    if (!currentUserId.value) return false
+    return row.createUserId === currentUserId.value
+}
+
+const isAssignee = (row) => {
+    if (!currentUserId.value) return false
+    if (!row.handleUsers) return false
+    return row.handleUsers.some(u => u.userId === currentUserId.value)
+}
+
+// Event Handlers
+const handleSearch = (params) => {
+    tableRef.value?.reload(params)
+}
+
+const handleAdd = () => {
+    editDialogRef.value?.handleOpen()
+}
+
+const handleBatchComplete = () => {
+    const selectedRows = tableRef.value?.getSelections()
+    
+    tableRef.value?.operatBatch({
+        method: 'post',
+        url: '/workorder/workorderinfo/finish',
+        title: `确认批量完成这 ${selectedRows.length} 个工单吗?`,
+        data: { ids: selectedRows.map(row => row.id) }
+    })
+}
+
+const handleEdit = (row) => {
+    editDialogRef.value?.handleOpen(row)
+}
+
+const refreshPage = () => {
+    tableRef.value?.reload()
+}
+
+const handleVoid = (row) => {
+    tableRef.value?.messageBoxConfirm({
+        message: '确认作废该工单吗?',
+        fetch: () => proxy.$http.post('/workorder/workorderinfo/cancel', { id: row.id })
+    })
+}
+
+const handleComplete = (row) => {
+    tableRef.value?.messageBoxConfirm({
+        message: '确认完成该工单吗?',
+        fetch: () => proxy.$http.post('/workorder/workorderinfo/finish', { ids: [row.id] })
+    })
+}
+
+const handleReopen = (row) => {
+    tableRef.value?.messageBoxConfirm({
+        message: '确认重开该工单吗?',
+        fetch: () => proxy.$http.post('/workorder/workorderinfo/reopen', { id: row.id })
+    })
+}
+</script>