WorkOrdersView.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. <template>
  2. <div class="p-4 md:p-8">
  3. <!-- 头部工具栏 -->
  4. <a-card class="mb-6 bg-gray-50 rounded-lg shadow-sm !border-0">
  5. <a-space size="large" wrap>
  6. <a-input-search
  7. v-model:value="searchKeyword"
  8. placeholder="搜索款号或工单号"
  9. class="w-full sm:w-80"
  10. @search="loadWorkOrders"
  11. />
  12. <a-select
  13. v-model:value="filterStatus"
  14. placeholder="状态筛选"
  15. class="w-full sm:w-40"
  16. allowClear
  17. @change="loadWorkOrders"
  18. >
  19. <a-select-option value="pending">待开始</a-select-option>
  20. <a-select-option value="in_progress">进行中</a-select-option>
  21. <a-select-option value="completed">已完成</a-select-option>
  22. <a-select-option value="cancelled">已取消</a-select-option>
  23. </a-select>
  24. <a-button type="primary" @click="showCreateModal" class="shadow-sm hover:shadow-md transition-shadow">
  25. <PlusOutlined class="mr-2" />
  26. 创建工单
  27. </a-button>
  28. </a-space>
  29. </a-card>
  30. <!-- 桌面端:工单列表表格 -->
  31. <a-card v-if="!layoutStore.isMobile" class="rounded-xl shadow-md !border-0">
  32. <a-table
  33. :columns="columns"
  34. :dataSource="workOrders"
  35. :loading="loading"
  36. :pagination="pagination"
  37. @change="handleTableChange"
  38. rowKey="id"
  39. >
  40. <template #status="{ record }">
  41. <a-tag :color="getStatusColor(record.status)" class="!rounded-full">
  42. {{ getStatusText(record.status) }}
  43. </a-tag>
  44. </template>
  45. <template #progress="{ record }">
  46. <a-progress
  47. :percent="Math.round((record.completed_qty / record.total_qty) * 100)"
  48. :status="record.status === 'completed' ? 'success' : 'active'"
  49. />
  50. </template>
  51. <template #action="{ record }">
  52. <a-space>
  53. <a-button size="small" @click="showEditModal(record)" class="hover:shadow-sm transition-shadow">
  54. <EditOutlined class="mr-1" />
  55. 编辑
  56. </a-button>
  57. <a-button size="small" type="default" @click="generateQRCode(record.id)" class="hover:shadow-sm transition-shadow">
  58. <QrcodeOutlined class="mr-1" />
  59. 生成二维码
  60. </a-button>
  61. <a-popconfirm
  62. title="确定删除此工单吗?"
  63. ok-text="确定"
  64. cancel-text="取消"
  65. @confirm="deleteWorkOrder(record.id)"
  66. >
  67. <a-button size="small" danger class="hover:shadow-sm transition-shadow">
  68. <DeleteOutlined class="mr-1" />
  69. 删除
  70. </a-button>
  71. </a-popconfirm>
  72. </a-space>
  73. </template>
  74. </a-table>
  75. </a-card>
  76. <!-- 移动端:工单卡片列表 -->
  77. <div v-else class="space-y-3">
  78. <a-spin :spinning="loading">
  79. <a-empty v-if="!loading && workOrders.length === 0" description="暂无工单数据" class="py-8" />
  80. <a-card
  81. v-for="item in workOrders"
  82. :key="item.id"
  83. class="rounded-xl shadow-sm !border-0"
  84. size="small"
  85. >
  86. <div class="flex items-start justify-between gap-2 mb-2">
  87. <span class="font-medium text-gray-900 break-all">{{ item.order_no }}</span>
  88. <a-tag :color="getStatusColor(item.status)" class="!rounded-full !mr-0 flex-shrink-0">
  89. {{ getStatusText(item.status) }}
  90. </a-tag>
  91. </div>
  92. <div class="text-sm text-gray-500 space-y-1 mb-2">
  93. <div>款号:{{ item.style }}</div>
  94. <div>款号名称:{{ item.style_name }}</div>
  95. <div>数量:{{ item.completed_qty }} / {{ item.total_qty }}</div>
  96. </div>
  97. <a-progress
  98. :percent="Math.round((item.completed_qty / item.total_qty) * 100)"
  99. :status="item.status === 'completed' ? 'success' : 'active'"
  100. size="small"
  101. class="mb-3"
  102. />
  103. <div class="flex items-center gap-2 pt-3 border-t border-gray-100">
  104. <a-button size="small" @click="showEditModal(item)">
  105. <EditOutlined class="mr-1" />
  106. 编辑
  107. </a-button>
  108. <a-button size="small" @click="generateQRCode(item.id)">
  109. <QrcodeOutlined class="mr-1" />
  110. 二维码
  111. </a-button>
  112. <a-popconfirm
  113. title="确定删除此工单吗?"
  114. ok-text="确定"
  115. cancel-text="取消"
  116. @confirm="deleteWorkOrder(item.id)"
  117. >
  118. <a-button size="small" danger>
  119. <DeleteOutlined class="mr-1" />
  120. 删除
  121. </a-button>
  122. </a-popconfirm>
  123. </div>
  124. </a-card>
  125. </a-spin>
  126. <div class="flex justify-center mt-4">
  127. <a-pagination
  128. v-model:current="pagination.current"
  129. v-model:pageSize="pagination.pageSize"
  130. :total="pagination.total"
  131. simple
  132. size="small"
  133. @change="handlePageChange"
  134. />
  135. </div>
  136. </div>
  137. <!-- 创建/编辑工单模态框 -->
  138. <a-modal
  139. v-model:open="modalVisible"
  140. :title="modalTitle"
  141. @ok="handleSubmit"
  142. @cancel="resetForm"
  143. :confirmLoading="submitting"
  144. :width="modalWidth"
  145. class="!rounded-xl"
  146. >
  147. <a-form
  148. ref="formRef"
  149. :model="formState"
  150. :rules="formRules"
  151. layout="vertical"
  152. >
  153. <a-form-item label="工单号" name="order_no" v-if="!isEdit">
  154. <a-input
  155. v-model:value="formState.order_no"
  156. placeholder="请输入工单号"
  157. />
  158. </a-form-item>
  159. <a-form-item label="款号" name="style" v-if="!isEdit">
  160. <a-input
  161. v-model:value="formState.style"
  162. placeholder="请输入款号(字母或数字)"
  163. />
  164. </a-form-item>
  165. <a-form-item label="款号名称" name="style_name">
  166. <a-input
  167. v-model:value="formState.style_name"
  168. placeholder="请输入款号名称"
  169. />
  170. </a-form-item>
  171. <a-form-item label="总数量" name="total_qty">
  172. <a-input-number
  173. v-model:value="formState.total_qty"
  174. :min="1"
  175. :max="100000"
  176. placeholder="请输入总数量"
  177. style="width: 100%"
  178. />
  179. </a-form-item>
  180. <a-form-item label="备注" name="remark">
  181. <a-textarea
  182. v-model:value="formState.remark"
  183. placeholder="可选备注信息"
  184. :rows="3"
  185. />
  186. </a-form-item>
  187. </a-form>
  188. </a-modal>
  189. <!-- 二维码展示模态框 -->
  190. <a-modal
  191. v-model:open="qrCodeModalVisible"
  192. title="工单二维码"
  193. @cancel="qrCodeModalVisible = false"
  194. :footer="null"
  195. :width="qrModalWidth"
  196. class="!rounded-xl"
  197. >
  198. <div class="text-center">
  199. <img :src="qrCodeUrl" alt="工单二维码" style="width: 100%" />
  200. <p class="mt-4 text-gray-600 text-sm">请使用手机扫描此二维码进行计件</p>
  201. </div>
  202. </a-modal>
  203. </div>
  204. </template>
  205. <script setup lang="ts">
  206. import { ref, reactive, onMounted, computed } from 'vue';
  207. import { message } from 'ant-design-vue';
  208. import { PlusOutlined, EditOutlined, DeleteOutlined, QrcodeOutlined } from '@ant-design/icons-vue';
  209. import { WorkOrderApi, createApiClient } from '@smartcut/api-client';
  210. import { STORAGE_KEYS } from '@smartcut/shared-utils';
  211. import { getApiBaseUrl } from '@/apiConfig';
  212. import { useLayoutStore } from '@/stores/layout';
  213. import type { WorkOrder } from '@smartcut/types';
  214. const apiClient = createApiClient({
  215. baseURL: getApiBaseUrl(),
  216. tokenKey: STORAGE_KEYS.FACTORY_TOKEN
  217. });
  218. const workOrderApi = new WorkOrderApi(apiClient);
  219. const layoutStore = useLayoutStore();
  220. const modalWidth = computed(() => layoutStore.isMobile ? '90%' : 600);
  221. const qrModalWidth = computed(() => layoutStore.isMobile ? '90%' : '400px');
  222. const workOrders = ref<WorkOrder[]>([]);
  223. const loading = ref(false);
  224. const searchKeyword = ref('');
  225. const filterStatus = ref('');
  226. const pagination = reactive({
  227. current: 1,
  228. pageSize: 10,
  229. total: 0,
  230. showSizeChanger: true,
  231. showTotal: (total: number) => `共 ${total} 条`
  232. });
  233. const columns = [
  234. { title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
  235. { title: '工单号', dataIndex: 'order_no', key: 'order_no', width: 150 },
  236. { title: '款号', dataIndex: 'style', key: 'style', width: 120 },
  237. { title: '款号名称', dataIndex: 'style_name', key: 'style_name', width: 200 },
  238. { title: '总数量', dataIndex: 'total_qty', key: 'total_qty', width: 100 },
  239. { title: '完成数量', dataIndex: 'completed_qty', key: 'completed_qty', width: 100 },
  240. { title: '进度', key: 'progress', width: 150, slots: { customRender: 'progress' } },
  241. { title: '状态', dataIndex: 'status', key: 'status', width: 100, slots: { customRender: 'status' } },
  242. { title: '操作', key: 'action', fixed: 'right', width: 200, slots: { customRender: 'action' } }
  243. ];
  244. const modalVisible = ref(false);
  245. const modalTitle = ref('创建工单');
  246. const isEdit = ref(false);
  247. const submitting = ref(false);
  248. const formRef = ref();
  249. const editingOrderId = ref<number | null>(null);
  250. const formState = reactive({
  251. order_no: '',
  252. style: '',
  253. style_name: '',
  254. total_qty: 1,
  255. remark: ''
  256. });
  257. const formRules = {
  258. order_no: [{ required: true, message: '请输入工单号', trigger: 'blur' }],
  259. style: [
  260. { required: true, message: '请输入款号', trigger: 'blur' },
  261. { pattern: /^[a-zA-Z0-9_-]+$/, message: '款号只能包含字母、数字、下划线或横线', trigger: 'blur' }
  262. ],
  263. style_name: [{ required: true, message: '请输入款号名称', trigger: 'blur' }],
  264. total_qty: [{ required: true, message: '请输入总数量', trigger: 'blur' }]
  265. };
  266. const qrCodeModalVisible = ref(false);
  267. const qrCodeUrl = ref('');
  268. function getStatusColor(status: string) {
  269. const colors: Record<string, string> = {
  270. pending: 'default',
  271. in_progress: 'blue',
  272. completed: 'green',
  273. cancelled: 'red'
  274. };
  275. return colors[status] || 'default';
  276. }
  277. function getStatusText(status: string) {
  278. const texts: Record<string, string> = {
  279. pending: '待开始',
  280. in_progress: '进行中',
  281. completed: '已完成',
  282. cancelled: '已取消'
  283. };
  284. return texts[status] || status;
  285. }
  286. async function loadWorkOrders() {
  287. loading.value = true;
  288. try {
  289. const result = await workOrderApi.getWorkOrders({
  290. page: pagination.current,
  291. page_size: pagination.pageSize,
  292. status: filterStatus.value,
  293. keyword: searchKeyword.value
  294. });
  295. workOrders.value = result.list;
  296. pagination.total = result.total;
  297. } catch (error) {
  298. message.error('加载工单列表失败');
  299. } finally {
  300. loading.value = false;
  301. }
  302. }
  303. function handleTableChange(pag: any) {
  304. pagination.current = pag.current;
  305. pagination.pageSize = pag.pageSize;
  306. loadWorkOrders();
  307. }
  308. // 移动端卡片列表分页变化
  309. function handlePageChange(page: number, pageSize: number) {
  310. pagination.current = page;
  311. pagination.pageSize = pageSize;
  312. loadWorkOrders();
  313. }
  314. function showCreateModal() {
  315. modalTitle.value = '创建工单';
  316. isEdit.value = false;
  317. modalVisible.value = true;
  318. resetForm();
  319. }
  320. function showEditModal(record: WorkOrder) {
  321. modalTitle.value = '编辑工单';
  322. isEdit.value = true;
  323. editingOrderId.value = record.id;
  324. modalVisible.value = true;
  325. formState.order_no = record.order_no;
  326. formState.style = record.style;
  327. formState.style_name = record.style_name;
  328. formState.total_qty = record.total_qty;
  329. formState.remark = record.remark;
  330. }
  331. async function handleSubmit() {
  332. try {
  333. await formRef.value.validate();
  334. submitting.value = true;
  335. if (isEdit.value && editingOrderId.value) {
  336. await workOrderApi.updateWorkOrder(editingOrderId.value, {
  337. style_name: formState.style_name,
  338. total_qty: formState.total_qty,
  339. remark: formState.remark
  340. });
  341. message.success('工单已更新');
  342. } else {
  343. await workOrderApi.createWorkOrder({
  344. order_no: formState.order_no,
  345. style: formState.style,
  346. style_name: formState.style_name,
  347. total_qty: formState.total_qty,
  348. remark: formState.remark
  349. });
  350. message.success('工单已创建');
  351. }
  352. modalVisible.value = false;
  353. loadWorkOrders();
  354. } catch (error: any) {
  355. const errorMsg = error.response?.data?.msg || '操作失败';
  356. message.error(errorMsg);
  357. } finally {
  358. submitting.value = false;
  359. }
  360. }
  361. function resetForm() {
  362. formRef.value?.resetFields();
  363. formState.order_no = '';
  364. formState.style = '';
  365. formState.style_name = '';
  366. formState.total_qty = 1;
  367. formState.remark = '';
  368. editingOrderId.value = null;
  369. }
  370. async function generateQRCode(id: number) {
  371. try {
  372. const blob = await workOrderApi.getWorkOrderQRCode(id);
  373. qrCodeUrl.value = URL.createObjectURL(blob);
  374. qrCodeModalVisible.value = true;
  375. } catch (error) {
  376. message.error('生成二维码失败');
  377. }
  378. }
  379. async function deleteWorkOrder(id: number) {
  380. try {
  381. await workOrderApi.deleteWorkOrder(id);
  382. message.success('工单已删除');
  383. loadWorkOrders();
  384. } catch (error) {
  385. message.error('删除工单失败');
  386. }
  387. }
  388. onMounted(() => {
  389. loadWorkOrders();
  390. });
  391. </script>