LogisticsProviderView.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. <template>
  2. <div class="app-page">
  3. <section class="glass-card section-card">
  4. <el-form :model="filters" inline class="filter-form">
  5. <el-form-item label="物流商名称">
  6. <el-input v-model="filters.name" placeholder="物流商名称" clearable style="width:160px" @keyup.enter="loadData" />
  7. </el-form-item>
  8. <el-form-item label="承运渠道">
  9. <el-select v-model="filters.channel" placeholder="全部" clearable style="width:140px">
  10. <el-option label="DHL" value="DHL" />
  11. <el-option label="FedEx" value="FedEx" />
  12. <el-option label="UPS" value="UPS" />
  13. <el-option label="顺丰" value="顺丰" />
  14. <el-option label="云途" value="云途" />
  15. </el-select>
  16. </el-form-item>
  17. <el-form-item label="状态">
  18. <el-select v-model="filters.status" placeholder="全部" clearable style="width:120px">
  19. <el-option label="启用" value="启用" />
  20. <el-option label="停用" value="停用" />
  21. </el-select>
  22. </el-form-item>
  23. <el-form-item>
  24. <el-button type="primary" @click="loadData">查询</el-button>
  25. <el-button @click="resetFilters">重置</el-button>
  26. </el-form-item>
  27. </el-form>
  28. </section>
  29. <section class="glass-card section-card" style="padding:12px 24px">
  30. <div class="table-toolbar" style="margin-bottom:0">
  31. <div class="chip-list">
  32. <el-button type="primary" @click="openDialog()">新建物流商</el-button>
  33. </div>
  34. <el-button @click="loadData">刷新</el-button>
  35. </div>
  36. </section>
  37. <section class="glass-card section-card">
  38. <el-table :data="filteredItems" stripe style="width:100%" v-loading="loading">
  39. <el-table-column prop="name" label="物流商名称" min-width="140" />
  40. <el-table-column prop="code" label="代码" width="100" />
  41. <el-table-column prop="channels" label="承运渠道" min-width="180">
  42. <template #default="{ row }">
  43. <el-tag v-for="c in row.channels" :key="c" size="small" style="margin-right:4px">{{ c }}</el-tag>
  44. </template>
  45. </el-table-column>
  46. <el-table-column prop="settlementType" label="结算方式" width="100" />
  47. <el-table-column prop="avgDays" label="平均时效(天)" width="110" />
  48. <el-table-column prop="trackingUrl" label="追踪URL" min-width="200" show-overflow-tooltip />
  49. <el-table-column prop="status" label="状态" width="80">
  50. <template #default="{ row }">
  51. <el-tag :type="row.status === '启用' ? 'success' : 'danger'" size="small">{{ row.status }}</el-tag>
  52. </template>
  53. </el-table-column>
  54. <el-table-column prop="updatedAt" label="更新时间" width="160" />
  55. <el-table-column label="操作" width="200" fixed="right">
  56. <template #default="{ row }">
  57. <el-button link type="primary" @click="openDialog(row)">编辑</el-button>
  58. <el-button link type="primary" @click="openTemplate(row)">运费模板</el-button>
  59. <el-button link :type="row.status === '启用' ? 'danger' : 'primary'" @click="toggleStatus(row)">{{ row.status === '启用' ? '停用' : '启用' }}</el-button>
  60. </template>
  61. </el-table-column>
  62. <template #empty>
  63. <el-empty description="暂无数据" />
  64. </template>
  65. </el-table>
  66. </section>
  67. <el-dialog v-model="dialogVisible" :title="editingItem ? '编辑物流商' : '新建物流商'" width="560px" destroy-on-close>
  68. <el-form :model="formData" label-width="110px">
  69. <el-form-item label="物流商名称" required>
  70. <el-input v-model="formData.name" placeholder="请输入物流商名称" />
  71. </el-form-item>
  72. <el-form-item label="物流商代码" required>
  73. <el-input v-model="formData.code" placeholder="如 DHL/FedEx" style="width:100%" />
  74. </el-form-item>
  75. <el-form-item label="承运渠道" required>
  76. <el-checkbox-group v-model="formData.channels">
  77. <el-checkbox label="DHL" />
  78. <el-checkbox label="FedEx" />
  79. <el-checkbox label="UPS" />
  80. <el-checkbox label="顺丰" />
  81. <el-checkbox label="云途" />
  82. <el-checkbox label="燕文" />
  83. <el-checkbox label="4PX" />
  84. </el-checkbox-group>
  85. </el-form-item>
  86. <el-form-item label="结算方式" required>
  87. <el-select v-model="formData.settlementType" placeholder="选择结算方式" style="width:100%">
  88. <el-option label="按重量" value="按重量" />
  89. <el-option label="按件数" value="按件数" />
  90. <el-option label="按体积" value="按体积" />
  91. <el-option label="包月" value="包月" />
  92. </el-select>
  93. </el-form-item>
  94. <el-form-item label="平均时效(天)">
  95. <el-input-number v-model="formData.avgDays" :min="1" :max="60" style="width:100%" />
  96. </el-form-item>
  97. <el-form-item label="追踪URL模板">
  98. <el-input v-model="formData.trackingUrl" placeholder="https://track.xxx.com/{trackingNo}" />
  99. </el-form-item>
  100. <el-form-item label="联系人">
  101. <el-input v-model="formData.contact" placeholder="联系人" />
  102. </el-form-item>
  103. <el-form-item label="联系电话">
  104. <el-input v-model="formData.phone" placeholder="电话" />
  105. </el-form-item>
  106. <el-form-item label="备注">
  107. <el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="备注信息" />
  108. </el-form-item>
  109. </el-form>
  110. <template #footer>
  111. <el-button @click="dialogVisible = false">取消</el-button>
  112. <el-button type="primary" @click="saveProvider">保存</el-button>
  113. </template>
  114. </el-dialog>
  115. <el-dialog v-model="templateDialogVisible" :title="`运费模板 - ${currentProvider?.name}`" width="700px" destroy-on-close>
  116. <div style="margin-bottom:16px">
  117. <el-button type="primary" size="small" @click="openTemplateDialog()">新建模板</el-button>
  118. </div>
  119. <el-table :data="templateList" stripe size="small">
  120. <el-table-column prop="name" label="模板名称" min-width="140" />
  121. <el-table-column prop="calcType" label="计费方式" width="100">
  122. <template #default="{ row }">
  123. <el-tag size="small">{{ row.calcType }}</el-tag>
  124. </template>
  125. </el-table-column>
  126. <el-table-column prop="firstWeight" label="首重" width="80" />
  127. <el-table-column prop="firstPrice" label="首费" width="80" />
  128. <el-table-column prop="continueWeight" label="续重" width="80" />
  129. <el-table-column prop="continuePrice" label="续费" width="80" />
  130. <el-table-column prop="regions" label="适用地区" min-width="120" show-overflow-tooltip />
  131. <el-table-column prop="status" label="状态" width="70">
  132. <template #default="{ row }">
  133. <el-tag :type="row.status === '启用' ? 'success' : 'danger'" size="small">{{ row.status }}</el-tag>
  134. </template>
  135. </el-table-column>
  136. <el-table-column label="操作" width="100" fixed="right">
  137. <template #default="{ row }">
  138. <el-button link type="primary" size="small" @click="editTemplate(row)">编辑</el-button>
  139. <el-button link type="danger" size="small" @click="deleteTemplate(row)">删除</el-button>
  140. </template>
  141. </el-table-column>
  142. </el-table>
  143. </el-dialog>
  144. <el-dialog v-model="templateFormVisible" :title="editingTemplate ? '编辑模板' : '新建模板'" width="500px" destroy-on-close>
  145. <el-form :model="templateForm" label-width="100px">
  146. <el-form-item label="模板名称" required>
  147. <el-input v-model="templateForm.name" placeholder="模板名称" />
  148. </el-form-item>
  149. <el-form-item label="计费方式" required>
  150. <el-select v-model="templateForm.calcType" placeholder="选择计费方式" style="width:100%">
  151. <el-option label="按重量" value="按重量" />
  152. <el-option label="按件数" value="按件数" />
  153. <el-option label="按体积" value="按体积" />
  154. </el-select>
  155. </el-form-item>
  156. <el-form-item label="首重/首件">
  157. <el-input-number v-model="templateForm.firstWeight" :min="0" style="width:48%" />
  158. <span style="margin:0 8px">kg/件</span>
  159. </el-form-item>
  160. <el-form-item label="首费">
  161. <el-input-number v-model="templateForm.firstPrice" :min="0" :precision="2" style="width:100%" />
  162. </el-form-item>
  163. <el-form-item label="续重/续件">
  164. <el-input-number v-model="templateForm.continueWeight" :min="0" style="width:48%" />
  165. <span style="margin:0 8px">kg/件</span>
  166. </el-form-item>
  167. <el-form-item label="续费">
  168. <el-input-number v-model="templateForm.continuePrice" :min="0" :precision="2" style="width:100%" />
  169. </el-form-item>
  170. <el-form-item label="适用地区">
  171. <el-input v-model="templateForm.regions" type="textarea" :rows="2" placeholder="美国,加拿大,英国" />
  172. </el-form-item>
  173. </el-form>
  174. <template #footer>
  175. <el-button @click="templateFormVisible = false">取消</el-button>
  176. <el-button type="primary" @click="saveTemplate">保存</el-button>
  177. </template>
  178. </el-dialog>
  179. </div>
  180. </template>
  181. <script setup lang="ts">
  182. import { computed, onMounted, reactive, ref } from 'vue';
  183. import { ElMessage, ElMessageBox } from 'element-plus';
  184. interface LogisticsProvider {
  185. id: string;
  186. name: string;
  187. code: string;
  188. channels: string[];
  189. settlementType: string;
  190. avgDays: number;
  191. trackingUrl: string;
  192. contact: string;
  193. phone: string;
  194. status: string;
  195. updatedAt: string;
  196. remark?: string;
  197. }
  198. interface ShippingTemplate {
  199. id: string;
  200. name: string;
  201. calcType: string;
  202. firstWeight: number;
  203. firstPrice: number;
  204. continueWeight: number;
  205. continuePrice: number;
  206. regions: string;
  207. status: string;
  208. }
  209. const items = ref<LogisticsProvider[]>([
  210. { id: 'LP001', name: 'DHL国际快递', code: 'DHL', channels: ['DHL'], settlementType: '按重量', avgDays: 5, trackingUrl: 'https://www.dhl.com/track?tracking-id={trackingNo}', contact: 'John', phone: '+1-800-225-5345', status: '启用', updatedAt: '2026-04-15 10:30' },
  211. { id: 'LP002', name: 'FedEx联邦快递', code: 'FedEx', channels: ['FedEx'], settlementType: '按重量', avgDays: 6, trackingUrl: 'https://www.fedex.com/fedextrack/?trknbr={trackingNo}', contact: 'Mary', phone: '+1-800-463-3339', status: '启用', updatedAt: '2026-04-14 14:20' },
  212. { id: 'LP003', name: '顺丰速运', code: 'SF', channels: ['顺丰'], settlementType: '按重量', avgDays: 3, trackingUrl: 'https://www.sf-express.com/sf-service-owf/web/en/querytools/track/{trackingNo}', contact: '王强', phone: '95338', status: '启用', updatedAt: '2026-04-10 09:00' },
  213. { id: 'LP004', name: '云途物流', code: 'YTO', channels: ['云途'], settlementType: '按重量', avgDays: 8, trackingUrl: 'https://www.yuntrack.com/track?trackingNo={trackingNo}', contact: 'Tom', phone: '400-800-6060', status: '停用', updatedAt: '2026-03-28 16:45' }
  214. ]);
  215. const templates = ref<ShippingTemplate[]>([
  216. { id: 'T001', name: 'DHL-美国专线', calcType: '按重量', firstWeight: 0.5, firstPrice: 120, continueWeight: 0.5, continuePrice: 35, regions: '美国,加拿大', status: '启用' },
  217. { id: 'T002', name: 'DHL-欧洲专线', calcType: '按重量', firstWeight: 0.5, firstPrice: 150, continueWeight: 0.5, continuePrice: 45, regions: '英国,德国,法国,意大利', status: '启用' },
  218. { id: 'T003', name: 'FedEx-全球优先', calcType: '按重量', firstWeight: 1, firstPrice: 200, continueWeight: 1, continuePrice: 50, regions: '全球', status: '启用' }
  219. ]);
  220. const loading = ref(false);
  221. const dialogVisible = ref(false);
  222. const templateDialogVisible = ref(false);
  223. const templateFormVisible = ref(false);
  224. const editingItem = ref<LogisticsProvider | null>(null);
  225. const currentProvider = ref<LogisticsProvider | null>(null);
  226. const editingTemplate = ref<ShippingTemplate | null>(null);
  227. const filters = ref({ name: '', channel: '', status: '' });
  228. const formData = reactive({
  229. name: '',
  230. code: '',
  231. channels: [] as string[],
  232. settlementType: '',
  233. avgDays: 5,
  234. trackingUrl: '',
  235. contact: '',
  236. phone: '',
  237. remark: ''
  238. });
  239. const templateForm = reactive({
  240. name: '',
  241. calcType: '按重量',
  242. firstWeight: 0.5,
  243. firstPrice: 0,
  244. continueWeight: 0.5,
  245. continuePrice: 0,
  246. regions: ''
  247. });
  248. const templateList = computed(() => {
  249. if (!currentProvider.value) return [];
  250. return templates.value;
  251. });
  252. const filteredItems = computed(() => {
  253. return items.value.filter(item => {
  254. if (filters.value.name && !item.name.includes(filters.value.name)) return false;
  255. if (filters.value.channel && !item.channels.includes(filters.value.channel)) return false;
  256. if (filters.value.status && item.status !== filters.value.status) return false;
  257. return true;
  258. });
  259. });
  260. const loadData = () => {
  261. loading.value = true;
  262. setTimeout(() => { loading.value = false; }, 300);
  263. };
  264. const resetFilters = () => {
  265. filters.value = { name: '', channel: '', status: '' };
  266. };
  267. const openDialog = (item?: LogisticsProvider) => {
  268. editingItem.value = item || null;
  269. if (item) {
  270. Object.assign(formData, { name: item.name, code: item.code, channels: [...item.channels], settlementType: item.settlementType, avgDays: item.avgDays, trackingUrl: item.trackingUrl, contact: item.contact, phone: item.phone, remark: item.remark || '' });
  271. } else {
  272. Object.assign(formData, { name: '', code: '', channels: [], settlementType: '', avgDays: 5, trackingUrl: '', contact: '', phone: '', remark: '' });
  273. }
  274. dialogVisible.value = true;
  275. };
  276. const saveProvider = () => {
  277. if (!formData.name || !formData.code || !formData.channels.length) {
  278. ElMessage.warning('请填写必填项');
  279. return;
  280. }
  281. if (editingItem.value) {
  282. const idx = items.value.findIndex(i => i.id === editingItem.value!.id);
  283. if (idx !== -1) items.value[idx] = { ...items.value[idx], ...formData, updatedAt: new Date().toLocaleString() };
  284. ElMessage.success('物流商已更新');
  285. } else {
  286. items.value.push({ id: `LP${String(items.value.length + 1).padStart(3, '0')}`, ...formData, status: '启用', updatedAt: new Date().toLocaleString() });
  287. ElMessage.success('物流商已创建');
  288. }
  289. dialogVisible.value = false;
  290. };
  291. const toggleStatus = async (row: LogisticsProvider) => {
  292. const action = row.status === '启用' ? '停用' : '启用';
  293. await ElMessageBox.confirm(`确认${action}「${row.name}」?`, `${action}确认`);
  294. const idx = items.value.findIndex(i => i.id === row.id);
  295. if (idx !== -1) {
  296. items.value[idx].status = row.status === '启用' ? '停用' : '启用';
  297. items.value[idx].updatedAt = new Date().toLocaleString();
  298. }
  299. ElMessage.success(`物流商已${action}`);
  300. };
  301. const openTemplate = (row: LogisticsProvider) => {
  302. currentProvider.value = row;
  303. templateDialogVisible.value = true;
  304. };
  305. const openTemplateDialog = () => {
  306. editingTemplate.value = null;
  307. Object.assign(templateForm, { name: '', calcType: '按重量', firstWeight: 0.5, firstPrice: 0, continueWeight: 0.5, continuePrice: 0, regions: '' });
  308. templateFormVisible.value = true;
  309. };
  310. const editTemplate = (row: ShippingTemplate) => {
  311. editingTemplate.value = row;
  312. Object.assign(templateForm, { ...row });
  313. templateFormVisible.value = true;
  314. };
  315. const saveTemplate = () => {
  316. if (!templateForm.name) {
  317. ElMessage.warning('请填写模板名称');
  318. return;
  319. }
  320. if (editingTemplate.value) {
  321. const idx = templates.value.findIndex(t => t.id === editingTemplate.value!.id);
  322. if (idx !== -1) templates.value[idx] = { ...templates.value[idx], ...templateForm };
  323. ElMessage.success('模板已更新');
  324. } else {
  325. templates.value.push({ id: `T${String(templates.value.length + 1).padStart(3, '0')}`, ...templateForm, status: '启用' });
  326. ElMessage.success('模板已创建');
  327. }
  328. templateFormVisible.value = false;
  329. };
  330. const deleteTemplate = async (row: ShippingTemplate) => {
  331. await ElMessageBox.confirm('确认删除此模板?', '删除确认');
  332. const idx = templates.value.findIndex(t => t.id === row.id);
  333. if (idx !== -1) templates.value.splice(idx, 1);
  334. ElMessage.success('模板已删除');
  335. };
  336. onMounted(loadData);
  337. </script>
  338. <style scoped>
  339. .filter-form :deep(.el-form-item) { margin-bottom: 0; }
  340. </style>