| | |
| | | "@iconify-json/ix": "^1.2.11", |
| | | "@iconify-json/line-md": "^1.2.16", |
| | | "@iconify-json/ri": "^1.2.10", |
| | | "@iconify-json/solar": "^1.2.5", |
| | | "@iconify-json/svg-spinners": "^1.2.4", |
| | | "@iconify-json/system-uicons": "^1.2.4", |
| | | "@iconify-json/vaadin": "^1.2.1", |
| | |
| | | '@iconify-json/ri': |
| | | specifier: ^1.2.10 |
| | | version: 1.2.10 |
| | | '@iconify-json/solar': |
| | | specifier: ^1.2.5 |
| | | version: 1.2.5 |
| | | '@iconify-json/svg-spinners': |
| | | specifier: ^1.2.4 |
| | | version: 1.2.4 |
| | |
| | | |
| | | '@iconify-json/ri@1.2.10': |
| | | resolution: {integrity: sha512-WWMhoncVVM+Xmu9T5fgu2lhYRrKTEWhKk3Com0KiM111EeEsRLiASjpsFKnC/SrB6covhUp95r2mH8tGxhgd5Q==} |
| | | |
| | | '@iconify-json/solar@1.2.5': |
| | | resolution: {integrity: sha512-WMAiNwchU8zhfrySww6KQBRIBbsQ6SvgIu2yA+CHGyMima/0KQwT5MXogrZPJGoQF+1Ye3Qj6K+1CiyNn3YkoA==} |
| | | |
| | | '@iconify-json/svg-spinners@1.2.4': |
| | | resolution: {integrity: sha512-ayn0pogFPwJA1WFZpDnoq9/hjDxN+keeCMyThaX4d3gSJ3y0mdKUxIA/b1YXWGtY9wVtZmxwcvOIeEieG4+JNg==} |
| | |
| | | dependencies: |
| | | '@iconify/types': 2.0.0 |
| | | |
| | | '@iconify-json/solar@1.2.5': |
| | | dependencies: |
| | | '@iconify/types': 2.0.0 |
| | | |
| | | '@iconify-json/svg-spinners@1.2.4': |
| | | dependencies: |
| | | '@iconify/types': 2.0.0 |
| | |
| | | import { icons as ixIcons } from '@iconify-json/ix' |
| | | import { icons as lineMdIcons } from '@iconify-json/line-md' |
| | | import { icons as remixIcons } from '@iconify-json/ri' |
| | | import { icons as solarIcons } from '@iconify-json/solar' |
| | | import { icons as svgSpinnersIcons } from '@iconify-json/svg-spinners' |
| | | import { icons as systemUiconsIcons } from '@iconify-json/system-uicons' |
| | | import { icons as vaadinIcons } from '@iconify-json/vaadin' |
| | |
| | | ix: ixIcons, |
| | | 'line-md': lineMdIcons, |
| | | ri: remixIcons, |
| | | solar: solarIcons, |
| | | 'svg-spinners': svgSpinnersIcons, |
| | | 'system-uicons': systemUiconsIcons, |
| | | vaadin: vaadinIcons |
| | |
| | | import request from '@/utils/http' |
| | | import { buildAsnOrderLogPageQueryParams } from '@/views/orders/asn-order-log/asnOrderLogPage.helpers' |
| | | |
| | | function normalizeText(value) { |
| | | return typeof value === 'string' ? value.trim() : value |
| | |
| | | } |
| | | |
| | | export function buildAsnOrderLogPageParams(params = {}) { |
| | | return { |
| | | current: params.current || 1, |
| | | pageSize: params.pageSize || params.size || 20, |
| | | ...filterParams(params, ['current', 'pageSize', 'size']) |
| | | } |
| | | return buildAsnOrderLogPageQueryParams(params) |
| | | } |
| | | |
| | | export function buildAsnOrderItemLogPageParams(params = {}) { |
| | |
| | | 'book-2-line': { |
| | | body: '<path fill="currentColor" d="M21 18H6a1 1 0 1 0 0 2h15v2H6a3 3 0 0 1-3-3V4a2 2 0 0 1 2-2h16zM5 16.05q.243-.05.5-.05H19V4H5zM16 9H8V7h8z"/>' |
| | | }, |
| | | 'chat-1-line': { |
| | | body: '<path fill="currentColor" d="M10 3h4a8 8 0 1 1 0 16v3.5c-5-2-12-5-12-11.5a8 8 0 0 1 8-8m2 14h2a6 6 0 0 0 0-12h-4a6 6 0 0 0-6 6c0 3.61 2.462 5.966 8 8.48z"/>' |
| | | 'chat-3-line': { |
| | | body: '<path fill="currentColor" d="M7.291 20.824L2 22l1.176-5.291A9.96 9.96 0 0 1 2 12C2 6.477 6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10a9.96 9.96 0 0 1-4.709-1.176m.29-2.113l.653.35A7.96 7.96 0 0 0 12 20a8 8 0 1 0-8-8c0 1.335.325 2.617.94 3.766l.349.653l-.655 2.947z"/>' |
| | | }, |
| | | 'chat-history-line': { |
| | | body: '<path fill="currentColor" d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.96 9.96 0 0 1-4.708-1.175L2 22l1.176-5.29A9.96 9.96 0 0 1 2 12C2 6.477 6.477 2 12 2m0 2a8 8 0 0 0-8 8c0 1.335.326 2.618.94 3.766l.35.654l-.656 2.946l2.948-.654l.653.349A7.96 7.96 0 0 0 12 20a8 8 0 1 0 0-16m1 3v5h4v2h-6V7z"/>' |
| | | }, |
| | | 'check-fill': { |
| | | body: '<path fill="currentColor" d="m10 15.17l9.192-9.191l1.414 1.414L10 17.999l-6.364-6.364l1.414-1.414z"/>' |
| | |
| | | 'close-large-fill': { |
| | | body: '<path fill="currentColor" d="M10.586 12L2.793 4.207l1.414-1.414L12 10.586l7.793-7.793l1.414 1.414L13.414 12l7.793 7.793l-1.414 1.414L12 13.414l-7.793 7.793l-1.414-1.414z"/>' |
| | | }, |
| | | 'close-line': { |
| | | body: '<path fill="currentColor" d="m12 10.587l4.95-4.95l1.414 1.414l-4.95 4.95l4.95 4.95l-1.415 1.414l-4.95-4.95l-4.949 4.95l-1.414-1.415l4.95-4.95l-4.95-4.95L7.05 5.638z"/>' |
| | | }, |
| | | 'coin-line': { |
| | | body: '<path fill="currentColor" d="M12.005 4.003c6.075 0 11 2.686 11 6v4c0 3.314-4.925 6-11 6c-5.967 0-10.824-2.591-10.995-5.823l-.005-.177v-4c0-3.314 4.925-6 11-6m0 12c-3.72 0-7.01-1.008-9-2.55v.55c0 1.882 3.883 4 9 4c5.01 0 8.838-2.03 8.995-3.882l.005-.118l.001-.55c-1.99 1.542-5.28 2.55-9.001 2.55m0-10c-5.117 0-9 2.118-9 4s3.883 4 9 4s9-2.118 9-4s-3.883-4-9-4"/>' |
| | | }, |
| | |
| | | 'computer-line': { |
| | | body: '<path fill="currentColor" d="M4 16h16V5H4zm9 2v2h4v2H7v-2h4v-2H2.992A1 1 0 0 1 2 16.992V4.008C2 3.451 2.455 3 2.992 3h18.016c.548 0 .992.449.992 1.007v12.985c0 .557-.455 1.008-.992 1.008z"/>' |
| | | }, |
| | | 'cpu-line': { |
| | | body: '<path fill="currentColor" d="M6 18h12V6H6zm8 2h-4v2H8v-2H5a1 1 0 0 1-1-1v-3H2v-2h2v-4H2V8h2V5a1 1 0 0 1 1-1h3V2h2v2h4V2h2v2h3a1 1 0 0 1 1 1v3h2v2h-2v4h2v2h-2v3a1 1 0 0 1-1 1h-3v2h-2zM8 8h8v8H8z"/>' |
| | | }, |
| | | 'delete-bin-4-line': { |
| | | body: '<path fill="currentColor" d="M20 7v14a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7H2V5h20v2zM6 7v13h12V7zm1-5h10v2H7zm4 8h2v7h-2z"/>' |
| | | }, |
| | |
| | | }, |
| | | 'delete-bin-6-line': { |
| | | body: '<path fill="currentColor" d="M7 4V2h10v2h5v2h-2v15a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6H2V4zM6 6v14h12V6zm3 3h2v8H9zm4 0h2v8h-2z"/>' |
| | | }, |
| | | 'delete-bin-line': { |
| | | body: '<path fill="currentColor" d="M17 6h5v2h-2v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V8H2V6h5V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1zm1 2H6v12h12zm-9 3h2v6H9zm4 0h2v6h-2zM9 4v2h6V4z"/>' |
| | | }, |
| | | 'drag-move-2-fill': { |
| | | body: '<path fill="currentColor" d="M18 11V8l4 4l-4 4v-3h-5v5h3l-4 4l-4-4h3v-5H6v3l-4-4l4-4v3h5V6H8l4-4l4 4h-3v5z"/>' |
| | |
| | | }, |
| | | 'edit-2-line': { |
| | | body: '<path fill="currentColor" d="M5 18.89h1.414l9.314-9.314l-1.414-1.414L5 17.476zm16 2H3v-4.243L16.435 3.212a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414L9.243 18.89H21zM15.728 6.748l1.414 1.414l1.414-1.414l-1.414-1.414z"/>' |
| | | }, |
| | | 'edit-line': { |
| | | body: '<path fill="currentColor" d="M6.414 15.89L16.556 5.748l-1.414-1.414L5 14.476v1.414zm.829 2H3v-4.243L14.435 2.212a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414zM3 19.89h18v2H3z"/>' |
| | | }, |
| | | 'error-warning-line': { |
| | | body: '<path fill="currentColor" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16m-1-5h2v2h-2zm0-8h2v6h-2z"/>' |
| | |
| | | 'fullscreen-line': { |
| | | body: '<path fill="currentColor" d="M8 3v2H4v4H2V3zM2 21v-6h2v4h4v2zm20 0h-6v-2h4v-4h2zm0-12h-2V5h-4V3h6z"/>' |
| | | }, |
| | | 'function-line': { |
| | | body: '<path fill="currentColor" d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1zm0 10a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1zM13 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1zm0 10a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1zm2-9v4h4V5zm0 10v4h4v-4zM5 5v4h4V5zm0 10v4h4v-4z"/>' |
| | | }, |
| | | 'github-line': { |
| | | body: '<path fill="currentColor" d="M5.884 18.653c-.3-.2-.558-.455-.86-.816a51 51 0 0 1-.466-.579c-.463-.575-.755-.841-1.056-.95a1 1 0 1 1 .675-1.882c.752.27 1.261.735 1.947 1.588c-.094-.117.34.427.433.539c.19.227.33.365.44.438c.204.137.588.196 1.15.14c.024-.382.094-.753.202-1.095c-2.968-.726-4.648-2.64-4.648-6.396c0-1.24.37-2.356 1.058-3.292c-.218-.894-.185-1.975.302-3.192a1 1 0 0 1 .63-.582c.081-.024.127-.035.208-.047c.803-.124 1.937.17 3.415 1.096a11.7 11.7 0 0 1 2.687-.308c.912 0 1.819.104 2.684.308c1.477-.933 2.614-1.227 3.422-1.096q.128.02.218.05a1 1 0 0 1 .616.58c.487 1.216.52 2.296.302 3.19c.691.936 1.058 2.045 1.058 3.293c0 3.757-1.674 5.665-4.642 6.392c.125.415.19.878.19 1.38c0 .665-.002 1.299-.007 2.01c0 .19-.002.394-.005.706a1 1 0 0 1-.018 1.958c-1.14.227-1.984-.532-1.984-1.525l.002-.447l.005-.705c.005-.707.008-1.337.008-1.997c0-.697-.184-1.152-.426-1.361c-.661-.57-.326-1.654.541-1.751c2.966-.333 4.336-1.482 4.336-4.66c0-.955-.312-1.744-.913-2.404A1 1 0 0 1 17.2 6.19c.166-.414.236-.957.095-1.614l-.01.003c-.491.139-1.11.44-1.858.949a1 1 0 0 1-.833.135a9.6 9.6 0 0 0-2.592-.349c-.89 0-1.772.118-2.592.35a1 1 0 0 1-.829-.134c-.753-.507-1.374-.807-1.87-.947c-.143.653-.072 1.194.093 1.607a1 1 0 0 1-.189 1.045c-.597.655-.913 1.458-.913 2.404c0 3.172 1.371 4.328 4.322 4.66c.865.097 1.202 1.177.545 1.748c-.193.168-.43.732-.43 1.364v3.15c0 .985-.834 1.725-1.96 1.528a1 1 0 0 1-.04-1.962v-.99c-.91.061-1.661-.088-2.254-.485"/>' |
| | | }, |
| | | 'group-line': { |
| | | body: '<path fill="currentColor" d="M2 22a8 8 0 1 1 16 0h-2a6 6 0 0 0-12 0zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6s6 2.685 6 6s-2.685 6-6 6m0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4s-4 1.79-4 4s1.79 4 4 4m8.284 3.703A8 8 0 0 1 23 22h-2a6 6 0 0 0-3.537-5.473zm-.688-11.29A5.5 5.5 0 0 1 21 8.5a5.5 5.5 0 0 1-5 5.478v-2.013a3.5 3.5 0 0 0 1.041-6.609z"/>' |
| | | }, |
| | |
| | | 'line-chart-line': { |
| | | body: '<path fill="currentColor" d="M5 3v16h16v2H3V3zm15.293 3.293l1.414 1.414L16 13.414l-3-2.999l-4.293 4.292l-1.414-1.414L13 7.586l3 2.999z"/>' |
| | | }, |
| | | 'list-check-3': { |
| | | body: '<path fill="currentColor" d="M8 6v3H5V6zM3 4v7h7V4zm10 0h8v2h-8zm0 7h8v2h-8zm0 7h8v2h-8zm-2.293-1.793l-1.414-1.414L6 18.086l-1.793-1.793l-1.414 1.414L6 20.914z"/>' |
| | | }, |
| | | 'lock-line': { |
| | | body: '<path fill="currentColor" d="M19 10h1a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V11a1 1 0 0 1 1-1h1V9a7 7 0 0 1 14 0zM5 12v8h14v-8zm6 2h2v4h-2zm6-4V9A5 5 0 0 0 7 9v1z"/>' |
| | | }, |
| | | 'magic-line': { |
| | | body: '<path fill="currentColor" d="M15.199 9.944a2.6 2.6 0 0 1-.79-1.55l-.403-3.083l-2.731 1.486a2.6 2.6 0 0 1-1.719.272L6.5 6.5l.57 3.056a2.6 2.6 0 0 1-.273 1.72l-1.486 2.73l3.083.403a2.6 2.6 0 0 1 1.55.79l2.138 2.257l1.336-2.807a2.6 2.6 0 0 1 1.23-1.231l2.808-1.336zm.025 5.564l-2.213 4.65a.6.6 0 0 1-.977.155l-3.542-3.739a.6.6 0 0 0-.358-.182l-5.106-.668a.6.6 0 0 1-.45-.881l2.462-4.524a.6.6 0 0 0 .063-.396L4.16 4.86a.6.6 0 0 1 .7-.7l5.062.943a.6.6 0 0 0 .397-.063l4.523-2.46a.6.6 0 0 1 .882.448l.668 5.107a.6.6 0 0 0 .182.357l3.739 3.542a.6.6 0 0 1-.155.977l-4.65 2.213a.6.6 0 0 0-.284.284m.797 1.927l1.414-1.414l4.243 4.242l-1.415 1.415z"/>' |
| | | }, |
| | | 'mail-line': { |
| | | body: '<path fill="currentColor" d="M3 3h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1m17 4.238l-7.928 7.1L4 7.216V19h16zM4.511 5l7.55 6.662L19.502 5z"/>' |
| | |
| | | 'more-2-fill': { |
| | | body: '<path fill="currentColor" d="M12 3c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2m0 14c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2m0-7c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2"/>' |
| | | }, |
| | | 'notification-2-line': { |
| | | body: '<path fill="currentColor" d="M22 20H2v-2h1v-6.969C3 6.043 7.03 2 12 2s9 4.043 9 9.031V18h1zM5 18h14v-6.969C19 7.148 15.866 4 12 4s-7 3.148-7 7.031zm4.5 3h5a2.5 2.5 0 0 1-5 0"/>' |
| | | 'node-tree': { |
| | | body: '<path fill="currentColor" d="M10 2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H8v2h5V9a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1v-1H8v6h5v-1a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1v-1H7a1 1 0 0 1-1-1V8H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1zm9 16h-4v2h4zm0-8h-4v2h4zM9 4H5v2h4z"/>' |
| | | }, |
| | | 'notification-3-line': { |
| | | body: '<path fill="currentColor" d="M20 17h2v2H2v-2h2v-7a8 8 0 1 1 16 0zm-2 0v-7a6 6 0 0 0-12 0v7zm-9 4h6v2H9z"/>' |
| | |
| | | 'search-line': { |
| | | body: '<path fill="currentColor" d="m18.031 16.617l4.283 4.282l-1.415 1.415l-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9s9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617m-2.006-.742A6.98 6.98 0 0 0 18 11c0-3.867-3.133-7-7-7s-7 3.133-7 7s3.133 7 7 7a6.98 6.98 0 0 0 4.875-1.975z"/>' |
| | | }, |
| | | 'send-plane-2-line': { |
| | | body: '<path fill="currentColor" d="M3.5 1.346a.5.5 0 0 1 .241.061l18.462 10.155a.5.5 0 0 1 0 .876L3.741 22.592A.5.5 0 0 1 3 22.154V1.846a.5.5 0 0 1 .5-.5M5 4.382V11h5v2H5v6.617L18.85 12z"/>' |
| | | }, |
| | | 'send-plane-line': { |
| | | body: '<path fill="currentColor" d="m21.727 2.957l-5.454 19.086c-.15.529-.475.553-.717.07L11 13L1.923 9.37c-.51-.205-.503-.51.034-.689L21.043 2.32c.529-.176.832.12.684.638m-2.692 2.14L6.812 9.17l5.637 2.255l3.04 6.08z"/>' |
| | | }, |
| | |
| | | }, |
| | | 'shield-check-line': { |
| | | body: '<path fill="currentColor" d="m12 1l8.217 1.826a1 1 0 0 1 .783.976v9.987a6 6 0 0 1-2.672 4.992L12 23l-6.328-4.219A6 6 0 0 1 3 13.79V3.802a1 1 0 0 1 .783-.976zm0 2.049L5 4.604v9.185a4 4 0 0 0 1.781 3.328L12 20.597l5.219-3.48A4 4 0 0 0 19 13.79V4.604zm4.452 5.173l1.415 1.414L11.503 16L7.26 11.757l1.414-1.414l2.828 2.828z"/>' |
| | | }, |
| | | 'sidebar-unfold-line': { |
| | | body: '<path fill="currentColor" d="M5 5h8v14H5zm14 14h-4V5h4zM4 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm7 9L7 8.5v7z"/>' |
| | | }, |
| | | 'smartphone-line': { |
| | | body: '<path fill="currentColor" d="M7 4v16h10V4zM6 2h12a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1m6 15a1 1 0 1 1 0 2a1 1 0 0 1 0-2"/>' |
| | |
| | | prefix: 'ri', |
| | | width: 24 |
| | | }, |
| | | solar: { |
| | | height: 24, |
| | | icons: { |
| | | 'double-alt-arrow-right-linear': { |
| | | body: '<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><path d="m11 19l6-7l-6-7"/><path d="m7 19l6-7l-6-7"/></g>' |
| | | } |
| | | }, |
| | | prefix: 'solar', |
| | | width: 24 |
| | | }, |
| | | vaadin: { |
| | | icons: { |
| | | 'ctrl-a': { |
| | |
| | | <template> |
| | | <div class="art-full-height flex flex-col gap-6"> |
| | | <section class="grid gap-6 md:grid-cols-2 xl:grid-cols-4" v-loading="sectionLoading.summary"> |
| | | <div class="art-full-height flex flex-col gap-6 lg:min-h-0 lg:overflow-hidden lg:gap-2"> |
| | | <section class="grid gap-3 md:grid-cols-2 lg:grid-cols-4 lg:shrink-0" v-loading="sectionLoading.summary"> |
| | | <div |
| | | v-for="item in summaryCardItems" |
| | | :key="item.title" |
| | | class="art-card flex items-start justify-between rounded-3xl px-7 py-6" |
| | | class="art-card flex items-start justify-between rounded-3xl px-4 py-4 lg:px-4 lg:py-3 xl:px-5" |
| | | > |
| | | <div class="min-w-0 pr-6"> |
| | | <p class="text-sm font-medium text-g-700">{{ item.title }}</p> |
| | | <ArtCountTo class="mt-3 block text-[2.3rem] font-semibold leading-none text-g-900" :target="item.count" :duration="1400" /> |
| | | <div class="mt-4 flex items-center gap-2 text-sm"> |
| | | <span :class="item.metaTone">{{ item.metaLabel }}</span> |
| | | <span class="text-g-500">{{ item.metaValue }}</span> |
| | | <div class="min-w-0 flex-1 pr-3"> |
| | | <p class="truncate text-sm font-medium text-g-700 lg:text-[13px]">{{ item.title }}</p> |
| | | <ArtCountTo |
| | | class="mt-1 block truncate text-[1.95rem] font-semibold leading-none text-g-900 lg:text-[1.72rem] xl:text-[1.92rem]" |
| | | :target="item.count" |
| | | :duration="1400" |
| | | /> |
| | | <div class="mt-1.5 flex items-center gap-1 text-xs lg:text-[12px] xl:text-sm"> |
| | | <span :class="item.metaTone" class="shrink-0">{{ item.metaLabel }}</span> |
| | | <span class="truncate text-g-500">{{ item.metaValue }}</span> |
| | | </div> |
| | | </div> |
| | | <div class="flex size-13 shrink-0 items-center justify-center rounded-2xl" :class="item.iconBoxClass"> |
| | | <ArtSvgIcon :icon="item.icon" class="text-2xl" :class="item.iconClass" /> |
| | | <div class="flex size-10 shrink-0 items-center justify-center rounded-2xl lg:size-9 xl:size-11" :class="item.iconBoxClass"> |
| | | <ArtSvgIcon :icon="item.icon" class="text-xl xl:text-2xl" :class="item.iconClass" /> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="grid gap-6 xl:grid-cols-[1.35fr_1fr]"> |
| | | <div class="art-card h-115 overflow-hidden p-6 box-border"> |
| | | <section class="flex min-h-0 flex-1 flex-col gap-2"> |
| | | <div class="grid min-h-0 flex-1 gap-2 lg:grid-cols-[1.35fr_1fr]"> |
| | | <div class="art-card box-border flex h-full min-h-0 flex-col overflow-hidden p-4 lg:p-5"> |
| | | <div class="art-card-header"> |
| | | <div class="title"> |
| | | <h4>近 30 天出入库趋势</h4> |
| | | <p>真实链路 <span class="text-success">已接通</span></p> |
| | | </div> |
| | | </div> |
| | | <div class="h-[calc(100%-4.5rem)]"> |
| | | <div class="mt-3 min-h-0 flex-1"> |
| | | <ArtBarChart |
| | | height="22rem" |
| | | height="100%" |
| | | :loading="sectionLoading.trend" |
| | | :data="trendChartSeries" |
| | | :x-axis-data="trendChartXAxisData" |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="art-card h-115 overflow-hidden p-6 box-border" v-loading="sectionLoading.locUsage"> |
| | | <div class="art-card box-border flex h-full min-h-0 flex-col overflow-hidden p-4 lg:p-5" v-loading="sectionLoading.locUsage"> |
| | | <div class="art-card-header"> |
| | | <div class="title"> |
| | | <h4>库位使用分布</h4> |
| | | <p>{{ usageLegendCount }} 个维度</p> |
| | | </div> |
| | | </div> |
| | | <div class="grid h-[calc(100%-4.5rem)] gap-6 lg:grid-cols-[1fr_0.95fr] lg:items-center"> |
| | | <div class="mt-3 grid min-h-0 flex-1 gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(220px,0.92fr)] lg:items-center"> |
| | | <div class="min-h-0 h-full"> |
| | | <ArtRingChart |
| | | height="21rem" |
| | | height="100%" |
| | | :data="locUsageList" |
| | | center-text="库位占比" |
| | | :show-legend="false" |
| | | :show-label="false" |
| | | /> |
| | | </div> |
| | | |
| | | <div class="space-y-1"> |
| | | <div class="min-h-0 overflow-hidden"> |
| | | <div |
| | | v-for="item in usageLegend" |
| | | :key="item.name" |
| | | class="flex items-center justify-between border-b border-[var(--art-border-color)] py-4 last:border-b-0" |
| | | class="flex items-center justify-between gap-3 border-b border-[var(--art-border-color)] py-3 last:border-b-0" |
| | | > |
| | | <div class="flex items-center gap-3"> |
| | | <span class="size-2.5 rounded-full" :style="{ backgroundColor: item.color }"></span> |
| | | <span class="text-sm text-[var(--art-gray-900)]">{{ item.name }}</span> |
| | | <span class="truncate text-sm text-[var(--art-gray-900)]">{{ item.name }}</span> |
| | | </div> |
| | | <span class="text-sm font-medium text-[var(--art-gray-700)]">{{ item.value }}%</span> |
| | | </div> |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | </div> |
| | | |
| | | <section class="grid gap-6 xl:grid-cols-[0.95fr_1.05fr]"> |
| | | <div class="art-card h-98 p-5 box-border" v-loading="sectionLoading.tasks"> |
| | | <div class="art-card box-border flex shrink-0 flex-col p-4 lg:p-3.5"> |
| | | <div class="art-card-header"> |
| | | <div class="title"> |
| | | <h4>执行中任务</h4> |
| | | <p>{{ taskSubtitle }}</p> |
| | | <h4>快捷跳转</h4> |
| | | <p>快速进入任务页和库存页</p> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="mt-3 h-[calc(100%-3.75rem)] overflow-hidden"> |
| | | <ElScrollbar> |
| | | <div |
| | | v-for="item in taskCardItems" |
| | | :key="`${item.time}-${item.title}`" |
| | | class="flex gap-4 border-b border-g-300 py-4 last:border-b-0" |
| | | <div class="mt-2.5 grid gap-2 md:grid-cols-2"> |
| | | <button |
| | | v-for="item in quickLinkCards" |
| | | :key="item.title" |
| | | type="button" |
| | | class="flex items-start justify-between rounded-3xl border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)] px-4 py-3 text-left transition hover:border-[var(--el-color-primary-light-5)] hover:bg-[var(--el-color-primary-light-9)]" |
| | | @click="navigateTo(item.path)" |
| | | > |
| | | <div class="flex flex-col items-center pt-1"> |
| | | <span class="size-3 rounded-full bg-[var(--el-color-primary)]"></span> |
| | | <span class="mt-2 min-h-10 w-px bg-[var(--art-border-color)]"></span> |
| | | <div class="min-w-0 pr-4"> |
| | | <p class="text-base font-medium text-[var(--art-gray-900)]">{{ item.title }}</p> |
| | | <p class="mt-0.5 text-sm text-g-500">{{ item.description }}</p> |
| | | </div> |
| | | <div class="min-w-0 flex-1"> |
| | | <p class="text-xs text-g-500">{{ item.time }}</p> |
| | | <div class="mt-2 flex items-center gap-2"> |
| | | <p class="truncate text-base font-medium text-[var(--art-gray-900)]"> |
| | | {{ item.title }} |
| | | </p> |
| | | <ElTag size="small" effect="light" :type="item.tagType">{{ item.tagText }}</ElTag> |
| | | <div class="flex size-11 shrink-0 items-center justify-center rounded-2xl" :class="item.iconBoxClass"> |
| | | <ArtSvgIcon :icon="item.icon" class="text-xl" :class="item.iconClass" /> |
| | | </div> |
| | | <p class="mt-2 text-sm text-g-600">{{ item.subtitle }}</p> |
| | | </div> |
| | | </div> |
| | | </ElScrollbar> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="art-card h-98 p-5 box-border" v-loading="sectionLoading.deadStock"> |
| | | <div class="art-card-header"> |
| | | <div class="title"> |
| | | <h4>库存最近动态</h4> |
| | | <p>{{ deadStockSubtitle }}</p> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="mt-3 h-[calc(100%-3.75rem)] overflow-hidden"> |
| | | <ElScrollbar> |
| | | <div |
| | | v-for="item in stockCardItems" |
| | | :key="`${item.title}-${item.time}`" |
| | | class="flex items-center gap-4 border-b border-g-300 py-4 last:border-b-0" |
| | | > |
| | | <div class="size-12 rounded-2xl bg-[var(--el-color-primary-light-9)] flex-cc"> |
| | | <ArtSvgIcon :icon="item.icon" class="text-xl text-[var(--el-color-primary)]" /> |
| | | </div> |
| | | <div class="min-w-0 flex-1"> |
| | | <p class="truncate text-base font-medium text-[var(--art-gray-900)]">{{ item.title }}</p> |
| | | <p class="mt-1 text-sm text-g-500">{{ item.status }}</p> |
| | | </div> |
| | | <div class="max-w-40 text-right text-sm text-g-500">{{ item.time }}</div> |
| | | </div> |
| | | </ElScrollbar> |
| | | </button> |
| | | </div> |
| | | </div> |
| | | </section> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { useRouter } from 'vue-router' |
| | | import { storeToRefs } from 'pinia' |
| | | import { useUserStore } from '@/store/modules/user' |
| | | import { |
| | | fetchDashboardHeader, |
| | | fetchDashboardTrend, |
| | | fetchDashboardDeadStock, |
| | | fetchDashboardLocUsage, |
| | | fetchDashboardTasks |
| | | fetchDashboardLocUsage |
| | | } from '@/api/dashboard' |
| | | import { |
| | | EMPTY_SUMMARY, |
| | | buildDashboardDeadStockQuery, |
| | | buildDashboardTaskQuery, |
| | | normalizeDashboardSummary, |
| | | normalizeDashboardTrend, |
| | | normalizeDashboardTaskList, |
| | | normalizeDashboardLocUsage, |
| | | normalizeDashboardDeadStockList, |
| | | withDashboardRequestGuard |
| | | } from './consolePage.helpers' |
| | | |
| | | defineOptions({ name: 'Console' }) |
| | | |
| | | const router = useRouter() |
| | | const summary = ref({ ...EMPTY_SUMMARY }) |
| | | const trendModel = ref(normalizeDashboardTrend()) |
| | | const taskList = ref([]) |
| | | const deadStockList = ref([]) |
| | | const locUsageList = ref([]) |
| | | const sectionLoading = reactive({ |
| | | summary: false, |
| | | trend: false, |
| | | deadStock: false, |
| | | locUsage: false, |
| | | tasks: false |
| | | locUsage: false |
| | | }) |
| | | |
| | | const userStore = useUserStore() |
| | | const { getUserInfo } = storeToRefs(userStore) |
| | | |
| | | const quickLinkCards = [ |
| | | { |
| | | title: '任务页', |
| | | description: '查看和处理任务管理数据', |
| | | path: '/manager/task', |
| | | icon: 'ri:task-line', |
| | | iconBoxClass: 'bg-[var(--el-color-primary-light-9)]', |
| | | iconClass: 'text-[var(--el-color-primary)]' |
| | | }, |
| | | { |
| | | title: '库存页', |
| | | description: '查看当前库存与库存明细', |
| | | path: '/manager/stock', |
| | | icon: 'ri:archive-stack-line', |
| | | iconBoxClass: 'bg-[rgba(20,222,186,0.14)]', |
| | | iconClass: 'text-[#14DEBA]' |
| | | } |
| | | ] |
| | | |
| | | const currentUser = computed(() => getUserInfo.value || {}) |
| | | const currentUserName = computed(() => { |
| | |
| | | }) |
| | | const trendChartXAxisData = computed(() => trendDisplayModel.value.xAxisData) |
| | | const trendChartSeries = computed(() => trendDisplayModel.value.series) |
| | | const taskSubtitle = computed(() => `最近 ${taskList.value.length} 条任务动态`) |
| | | const deadStockSubtitle = computed(() => `最近 ${deadStockList.value.length} 条库存记录`) |
| | | const usageLegendCount = computed(() => locUsageList.value.length) |
| | | const usageLegend = computed(() => { |
| | | const palette = ['#5B8FF9', '#5AD8A6', '#5D7092', '#F6BD16', '#E8684A', '#6DC8EC'] |
| | |
| | | color: palette[index % palette.length] |
| | | })) |
| | | }) |
| | | const taskCardItems = computed(() => |
| | | taskList.value.slice(0, 6).map((item) => ({ |
| | | title: item.title, |
| | | time: item.time, |
| | | subtitle: item.status, |
| | | tagText: resolveTaskTagText(item.status), |
| | | tagType: resolveTaskTagType(item.class) |
| | | })) |
| | | ) |
| | | const stockCardItems = computed(() => deadStockList.value.slice(0, 6)) |
| | | |
| | | onMounted(() => { |
| | | loadDashboard() |
| | |
| | | function loadDashboard() { |
| | | void loadSummarySection() |
| | | void loadTrendSection() |
| | | void loadDeadStockSection() |
| | | void loadLocUsageSection() |
| | | void loadTaskSection() |
| | | } |
| | | |
| | | async function loadSummarySection() { |
| | |
| | | sectionLoading.trend = false |
| | | } |
| | | |
| | | async function loadDeadStockSection() { |
| | | sectionLoading.deadStock = true |
| | | const payload = await withDashboardRequestGuard( |
| | | fetchDashboardDeadStock(buildDashboardDeadStockQuery()), |
| | | {} |
| | | ) |
| | | deadStockList.value = normalizeDashboardDeadStockList(payload || {}) |
| | | sectionLoading.deadStock = false |
| | | } |
| | | |
| | | async function loadLocUsageSection() { |
| | | sectionLoading.locUsage = true |
| | | const payload = await withDashboardRequestGuard(fetchDashboardLocUsage(), null) |
| | |
| | | sectionLoading.locUsage = false |
| | | } |
| | | |
| | | async function loadTaskSection() { |
| | | sectionLoading.tasks = true |
| | | const payload = await withDashboardRequestGuard( |
| | | fetchDashboardTasks(buildDashboardTaskQuery()), |
| | | {} |
| | | ) |
| | | taskList.value = normalizeDashboardTaskList(payload || {}) |
| | | sectionLoading.tasks = false |
| | | } |
| | | |
| | | function resolveTaskTagType(statusClass) { |
| | | const text = String(statusClass || '') |
| | | if (text.includes('emerald')) return 'success' |
| | | if (text.includes('rose')) return 'danger' |
| | | return 'primary' |
| | | } |
| | | |
| | | function resolveTaskTagText(statusText) { |
| | | const text = String(statusText || '') |
| | | .replace(/\b\d+\./g, '') |
| | | .replace(/\s+/g, ' ') |
| | | .trim() |
| | | const parts = text.split('·').map((item) => item.trim()).filter(Boolean) |
| | | return parts[parts.length - 1] || '处理中' |
| | | function navigateTo(path) { |
| | | if (!path) return |
| | | router.push(path) |
| | | } |
| | | </script> |
| | |
| | | density: 'compact', |
| | | showSequence: true |
| | | } |
| | | export const DEFAULT_ASN_ORDER_LOG_PAGE_SIZE = 20 |
| | | |
| | | const RLE_STATUS_META = { |
| | | 0: { text: '正常', type: 'info' }, |
| | |
| | | } |
| | | const parsed = Number(value) |
| | | return Number.isNaN(parsed) ? fallback : parsed |
| | | } |
| | | |
| | | function normalizePositiveInteger(value, fallback) { |
| | | const parsed = normalizeNumber(value, fallback) |
| | | if (!Number.isInteger(parsed) || parsed <= 0) { |
| | | return fallback |
| | | } |
| | | return parsed |
| | | } |
| | | |
| | | function normalizeDateText(value) { |
| | |
| | | |
| | | export function buildAsnOrderLogPageQueryParams(params = {}) { |
| | | return { |
| | | current: params.current || 1, |
| | | pageSize: params.pageSize || params.size || 20, |
| | | pageSize: normalizePositiveInteger( |
| | | params.pageSize ?? params.size, |
| | | DEFAULT_ASN_ORDER_LOG_PAGE_SIZE |
| | | ), |
| | | ...(params.cursor !== undefined && params.cursor !== null && params.cursor !== '' |
| | | ? { cursor: normalizeNumber(params.cursor) } |
| | | : {}), |
| | | ...buildAsnOrderLogSearchParams(params) |
| | | } |
| | | } |
| | |
| | | return { |
| | | logId: params.logId, |
| | | current: params.current || 1, |
| | | pageSize: params.pageSize || params.size || 20 |
| | | pageSize: params.pageSize || params.size || DEFAULT_ASN_ORDER_LOG_PAGE_SIZE |
| | | } |
| | | } |
| | | |
| | |
| | | :data="data" |
| | | :columns="columns" |
| | | :pagination="pagination" |
| | | :pagination-options="mainPaginationOptions" |
| | | @selection-change="handleSelectionChange" |
| | | @pagination:size-change="handleSizeChange" |
| | | @pagination:current-change="handleCurrentChange" |
| | |
| | | import { computed, onMounted, reactive, ref } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { useUserStore } from '@/store/modules/user' |
| | | import { useTable } from '@/hooks/core/useTable' |
| | | import { useTableColumns } from '@/hooks/core/useTableColumns' |
| | | import { usePrintExportPage } from '@/views/system/common/usePrintExportPage' |
| | | import ListExportPrint from '@/components/biz/list-export-print/index.vue' |
| | | import { defaultResponseAdapter } from '@/utils/table/tableUtils' |
| | | import { guardRequestWithMessage } from '@/utils/sys/requestGuard' |
| | | import { fetchDictDataPage } from '@/api/system-manage' |
| | | import { |
| | | DEFAULT_ASN_ORDER_LOG_PAGE_SIZE, |
| | | buildAsnOrderLogDetailQueryParams, |
| | | buildAsnOrderLogPageQueryParams, |
| | | buildAsnOrderLogPrintRows, |
| | | buildAsnOrderLogReportMeta, |
| | | buildAsnOrderLogSearchParams, |
| | | createAsnOrderLogSearchState, |
| | | getAsnOrderLogExceStatusOptions, |
| | | getAsnOrderLogNtyStatusOptions, |
| | | getAsnOrderLogRleStatusOptions, |
| | | getAsnOrderLogStatusOptions, |
| | |
| | | const reportTitle = ASN_ORDER_LOG_REPORT_TITLE |
| | | const searchForm = ref(createAsnOrderLogSearchState()) |
| | | const selectedRows = ref([]) |
| | | const data = ref([]) |
| | | const loading = ref(false) |
| | | const detailDrawerVisible = ref(false) |
| | | const detailLoading = ref(false) |
| | | const detailItemsLoading = ref(false) |
| | |
| | | const wkTypeOptions = ref([]) |
| | | const exceStatusOptions = ref([]) |
| | | const detailItemColumns = createAsnOrderItemLogColumns() |
| | | const pageSize = ref(DEFAULT_ASN_ORDER_LOG_PAGE_SIZE) |
| | | const cursorHistory = ref([null]) |
| | | const nextCursor = ref(null) |
| | | const hasNext = ref(false) |
| | | |
| | | const detailPagination = reactive({ |
| | | current: 1, |
| | |
| | | }) |
| | | |
| | | const reportQueryParams = computed(() => buildAsnOrderLogSearchParams(searchForm.value)) |
| | | const mainPaginationOptions = { |
| | | layout: 'prev, next, sizes', |
| | | hideOnSinglePage: false |
| | | } |
| | | const pagination = computed(() => { |
| | | const current = Math.max(1, cursorHistory.value.length || 1) |
| | | const size = Number(pageSize.value) > 0 ? Number(pageSize.value) : DEFAULT_ASN_ORDER_LOG_PAGE_SIZE |
| | | const recordCount = Math.max(0, Number(data.value.length) || 0) |
| | | const total = hasNext.value ? current * size + 1 : (current - 1) * size + recordCount |
| | | |
| | | return { |
| | | current, |
| | | size, |
| | | total |
| | | } |
| | | }) |
| | | |
| | | const searchItems = computed(() => [ |
| | | { |
| | |
| | | } |
| | | ]) |
| | | |
| | | function updatePaginationState(target, response, fallbackCurrent, fallbackSize) { |
| | | target.total = Number(response?.total || 0) |
| | | target.current = Number(response?.current || fallbackCurrent || 1) |
| | | target.size = Number(response?.size || fallbackSize || target.size || 20) |
| | | const { columns, columnChecks } = useTableColumns(() => |
| | | createAsnOrderLogTableColumns({ handleView: openDetail }) |
| | | ) |
| | | |
| | | async function loadMainList({ history = cursorHistory.value } = {}) { |
| | | loading.value = true |
| | | try { |
| | | const normalizedHistory = |
| | | Array.isArray(history) && history.length > 0 ? [...history] : [null] |
| | | const response = await guardRequestWithMessage( |
| | | fetchAsnOrderLogPage( |
| | | buildAsnOrderLogPageQueryParams({ |
| | | ...searchForm.value, |
| | | cursor: normalizedHistory[normalizedHistory.length - 1] ?? null, |
| | | pageSize: pageSize.value |
| | | }) |
| | | ), |
| | | { |
| | | records: [], |
| | | pageSize: pageSize.value, |
| | | nextCursor: null, |
| | | hasNext: false |
| | | }, |
| | | { |
| | | timeoutMessage: '历史通知单列表加载超时,已停止等待' |
| | | } |
| | | ) |
| | | |
| | | cursorHistory.value = normalizedHistory |
| | | data.value = Array.isArray(response?.records) |
| | | ? response.records.map((item) => normalizeAsnOrderLogRow(item)) |
| | | : [] |
| | | nextCursor.value = response?.nextCursor ?? null |
| | | hasNext.value = Boolean( |
| | | response?.hasNext && response?.nextCursor !== null && response?.nextCursor !== undefined |
| | | ) |
| | | selectedRows.value = [] |
| | | } catch (error) { |
| | | ElMessage.error(error?.message || '获取历史通知单列表失败') |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | function openDetail(row) { |
| | |
| | | detailDrawerVisible.value = true |
| | | loadDetailResources() |
| | | } |
| | | |
| | | const { |
| | | columns, |
| | | columnChecks, |
| | | data, |
| | | loading, |
| | | pagination, |
| | | replaceSearchParams, |
| | | resetSearchParams, |
| | | handleSizeChange, |
| | | handleCurrentChange, |
| | | refreshData, |
| | | getData |
| | | } = useTable({ |
| | | core: { |
| | | apiFn: fetchAsnOrderLogPage, |
| | | apiParams: buildAsnOrderLogPageQueryParams(searchForm.value), |
| | | columnsFactory: () => createAsnOrderLogTableColumns({ handleView: openDetail }) |
| | | }, |
| | | transform: { |
| | | dataTransformer: (records) => |
| | | Array.isArray(records) ? records.map((item) => normalizeAsnOrderLogRow(item)) : [] |
| | | } |
| | | }) |
| | | |
| | | async function loadDetailResources() { |
| | | if (!activeLogId.value) { |
| | |
| | | detailItemRows.value = Array.isArray(itemResult.records) |
| | | ? itemResult.records.map((item) => normalizeAsnOrderItemLogRow(item)) |
| | | : [] |
| | | updatePaginationState( |
| | | detailPagination, |
| | | itemResult, |
| | | detailPagination.current, |
| | | detailPagination.size |
| | | ) |
| | | detailPagination.total = Number(itemResult?.total || 0) |
| | | detailPagination.current = Number(itemResult?.current || detailPagination.current || 1) |
| | | detailPagination.size = Number(itemResult?.size || detailPagination.size || 20) |
| | | } catch (error) { |
| | | detailDrawerVisible.value = false |
| | | detailData.value = {} |
| | |
| | | selectedRows.value = Array.isArray(rows) ? rows : [] |
| | | } |
| | | |
| | | function handleSearch(params) { |
| | | async function handleSearch(params) { |
| | | searchForm.value = { |
| | | ...searchForm.value, |
| | | ...params |
| | | } |
| | | replaceSearchParams(buildAsnOrderLogSearchParams(searchForm.value)) |
| | | getData() |
| | | await loadMainList({ history: [null] }) |
| | | } |
| | | |
| | | function handleReset() { |
| | | async function handleReset() { |
| | | searchForm.value = createAsnOrderLogSearchState() |
| | | resetSearchParams() |
| | | await loadMainList({ history: [null] }) |
| | | } |
| | | |
| | | async function handleSizeChange(size) { |
| | | if (Number(size) <= 0) { |
| | | return |
| | | } |
| | | pageSize.value = Number(size) |
| | | await loadMainList({ history: [null] }) |
| | | } |
| | | |
| | | async function handleCurrentChange(current) { |
| | | const targetPage = Number(current) |
| | | const currentPage = Number(pagination.value.current) |
| | | |
| | | if (!Number.isInteger(targetPage) || targetPage <= 0 || targetPage === currentPage) { |
| | | return |
| | | } |
| | | if (loading.value) { |
| | | return |
| | | } |
| | | |
| | | if (targetPage === currentPage + 1) { |
| | | if (!hasNext.value || nextCursor.value === null || nextCursor.value === undefined) { |
| | | return |
| | | } |
| | | const nextHistory = [...cursorHistory.value, Number(nextCursor.value)] |
| | | await loadMainList({ history: nextHistory }) |
| | | return |
| | | } |
| | | |
| | | if (targetPage === currentPage - 1) { |
| | | await loadMainList({ |
| | | history: cursorHistory.value.length > 1 ? cursorHistory.value.slice(0, -1) : [null] |
| | | }) |
| | | } |
| | | } |
| | | |
| | | async function refreshData() { |
| | | await loadMainList() |
| | | } |
| | | |
| | | function handleDetailSizeChange(size) { |
| | |
| | | } |
| | | } |
| | | |
| | | async function fetchAllPrintableRecords(queryParams = {}, maxResults = 1000) { |
| | | const records = [] |
| | | let cursor = null |
| | | const batchSize = Math.max(Number(pageSize.value) || DEFAULT_ASN_ORDER_LOG_PAGE_SIZE, 100) |
| | | |
| | | while (records.length < maxResults) { |
| | | const response = await fetchAsnOrderLogPage( |
| | | buildAsnOrderLogPageQueryParams({ |
| | | ...queryParams, |
| | | cursor, |
| | | pageSize: batchSize |
| | | }) |
| | | ) |
| | | const pageRecords = Array.isArray(response?.records) ? response.records : [] |
| | | |
| | | if (pageRecords.length === 0) { |
| | | break |
| | | } |
| | | |
| | | records.push(...pageRecords) |
| | | |
| | | if (!response?.hasNext || response?.nextCursor === null || response?.nextCursor === undefined) { |
| | | break |
| | | } |
| | | cursor = response.nextCursor |
| | | } |
| | | |
| | | return records.slice(0, maxResults) |
| | | } |
| | | |
| | | async function resolvePrintRecords(payload) { |
| | | if (Array.isArray(payload?.ids) && payload.ids.length > 0) { |
| | | return defaultResponseAdapter(await fetchGetAsnOrderLogMany(payload.ids)).records |
| | | } |
| | | return defaultResponseAdapter( |
| | | await fetchAsnOrderLogPage({ |
| | | ...reportQueryParams.value, |
| | | current: 1, |
| | | pageSize: |
| | | Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20 |
| | | }) |
| | | ).records |
| | | return fetchAllPrintableRecords(reportQueryParams.value) |
| | | } |
| | | |
| | | const { |
| | |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | await Promise.allSettled([loadTypeOptions(), loadWkTypeOptions(), loadExceStatusOptions()]) |
| | | await Promise.allSettled([ |
| | | loadTypeOptions(), |
| | | loadWkTypeOptions(), |
| | | loadExceStatusOptions(), |
| | | loadMainList() |
| | | ]) |
| | | }) |
| | | </script> |
| New file |
| | |
| | | package com.vincent.rsf.server.common.domain; |
| | | |
| | | import lombok.Data; |
| | | import lombok.EqualsAndHashCode; |
| | | |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 通用游标分页参数。 |
| | | * |
| | | * <p>这个类在现有 {@link BaseParam} 的基础上只增加一个 {@code cursor} 字段, |
| | | * 用来表示“下一页从哪里继续查”。</p> |
| | | * |
| | | * <p>当前项目里的普通分页接口大多还是 {@code current + pageSize} 模式, |
| | | * 但游标分页只依赖:</p> |
| | | * <ul> |
| | | * <li>{@code pageSize}:每页大小</li> |
| | | * <li>{@code cursor}:上一页最后一条记录的游标值</li> |
| | | * </ul> |
| | | * |
| | | * <p>这里仍然继承 {@link BaseParam},目的是继续复用原有的:</p> |
| | | * <ul> |
| | | * <li>筛选条件解析</li> |
| | | * <li>{@code condition / timeStart / timeEnd} 等通用参数</li> |
| | | * <li>和 {@link com.vincent.rsf.server.common.domain.PageParam} 的协作能力</li> |
| | | * </ul> |
| | | * |
| | | * <p>注意:前端即使继续传 {@code current},这里也不会报错; |
| | | * {@link BaseParam#syncMap(Map)} 会正常解析它,但后续通用游标分页逻辑不会使用它。</p> |
| | | */ |
| | | @Data |
| | | @EqualsAndHashCode(callSuper = true) |
| | | public class CursorPageParam extends BaseParam { |
| | | |
| | | /** |
| | | * 当前页的起始游标。 |
| | | * |
| | | * <p>约定含义是:只查询“比这个游标更旧”的数据, |
| | | * 例如按 {@code id desc} 翻页时,会生成 {@code id < cursor} 的条件。</p> |
| | | */ |
| | | private Long cursor; |
| | | |
| | | @Override |
| | | public void syncMap(Map<String, Object> map) { |
| | | // 先复用 BaseParam 的通用参数解析能力, |
| | | // 保证 pageSize、condition、timeStart、timeEnd 等字段照常工作。 |
| | | super.syncMap(map); |
| | | if (map == null) { |
| | | return; |
| | | } |
| | | Object cursorValue = map.get("cursor"); |
| | | if (cursorValue == null) { |
| | | return; |
| | | } |
| | | String normalizedValue = String.valueOf(cursorValue).trim(); |
| | | if (normalizedValue.isEmpty()) { |
| | | // 空字符串游标等价于“首屏没有游标”,这里直接移除,避免后续误参与 SQL。 |
| | | map.remove("cursor"); |
| | | return; |
| | | } |
| | | // 目前通用方案统一把游标约束为 Long, |
| | | // 这样可以覆盖当前按主键/数字字段倒序翻页的业务场景。 |
| | | this.cursor = Long.parseLong(normalizedValue); |
| | | // 解析完成后要从 map 中移除,避免 PageParam.buildWrapper(true) |
| | | // 把 cursor 当成普通筛选字段再次拼进 where 条件。 |
| | | map.remove("cursor"); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.common.domain; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 通用游标分页返回体。 |
| | | * |
| | | * <p>对外响应结构刻意保持简单,只暴露游标分页真正需要的 4 个字段, |
| | | * 不再返回传统页码分页里的 {@code total/current/pages}。</p> |
| | | * |
| | | * <p>字段语义约定:</p> |
| | | * <ul> |
| | | * <li>{@code records}:当前页实际返回的数据</li> |
| | | * <li>{@code pageSize}:本次查询最终采用的分页大小(包含默认值回退后的结果)</li> |
| | | * <li>{@code nextCursor}:下一页应当使用的游标;没有下一页时为 {@code null}</li> |
| | | * <li>{@code hasNext}:是否还有下一页</li> |
| | | * </ul> |
| | | * |
| | | * <p>这样设计的目的是让 controller 直接 {@code R.ok().add(result)}, |
| | | * 同时让前端只关心“有没有下一页”和“下一页该带什么 cursor”。</p> |
| | | */ |
| | | @Data |
| | | public class CursorPageResult<T> { |
| | | |
| | | /** 当前页数据列表。 */ |
| | | private List<T> records; |
| | | |
| | | /** 当前请求最终生效的分页大小。 */ |
| | | private Integer pageSize; |
| | | |
| | | /** |
| | | * 下一页要携带的游标值。 |
| | | * |
| | | * <p>约定取“当前页最后一条记录”的游标字段值。 |
| | | * 如果没有下一页,则返回 null。</p> |
| | | */ |
| | | private Long nextCursor; |
| | | |
| | | /** 是否还存在下一页。 */ |
| | | private Boolean hasNext; |
| | | } |
| | |
| | | import com.vincent.rsf.framework.common.R; |
| | | import com.vincent.rsf.server.common.utils.ExcelUtil; |
| | | import com.vincent.rsf.server.common.annotation.OperationLog; |
| | | import com.vincent.rsf.server.common.domain.BaseParam; |
| | | import com.vincent.rsf.server.common.domain.CursorPageParam; |
| | | import com.vincent.rsf.server.common.domain.KeyValVo; |
| | | import com.vincent.rsf.server.common.domain.PageParam; |
| | | import com.vincent.rsf.server.manager.entity.AsnOrderLog; |
| | | import com.vincent.rsf.server.manager.service.AsnOrderLogService; |
| | | import com.vincent.rsf.server.manager.utils.buildPageRowsUtils; |
| | | import com.vincent.rsf.server.system.controller.BaseController; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | |
| | | @RestController |
| | | public class AsnOrderLogController extends BaseController { |
| | | |
| | | private static final int DEFAULT_CURSOR_PAGE_SIZE = 20; |
| | | |
| | | @Autowired |
| | | private AsnOrderLogService asnOrderLogService; |
| | | |
| | | @PreAuthorize("hasAuthority('manager:asnOrderLog:list')") |
| | | @PostMapping("/asnOrderLog/page") |
| | | public R page(@RequestBody Map<String, Object> map) { |
| | | BaseParam baseParam = buildParam(map, BaseParam.class); |
| | | PageParam<AsnOrderLog, BaseParam> pageParam = new PageParam<>(baseParam, AsnOrderLog.class); |
| | | return R.ok().add(asnOrderLogService.page(pageParam, pageParam.buildWrapper(true))); |
| | | // 这里已经不再手写游标分页细节,而是直接复用 BaseController 的通用实现。 |
| | | // |
| | | // 当前这几个参数分别表达: |
| | | // 1. map:前端原始请求参数 |
| | | // 2. CursorPageParam.class:通用游标参数解析器 |
| | | // 3. AsnOrderLog.class:用于通用筛选和 condition 模糊搜索 |
| | | // 4. asnOrderLogService:实际执行查询的 service |
| | | // 5. "id":本接口的游标字段,固定按 id desc 做分页 |
| | | // 6. DEFAULT_CURSOR_PAGE_SIZE:默认每页大小 |
| | | // 7. null:当前没有额外的 where 条件扩展 |
| | | // 8. buildPageRowsUtils::userNameMap:结果后处理,批量补 createBy$/updateBy$ |
| | | return R.ok().add(cursorPage( |
| | | map, |
| | | CursorPageParam.class, |
| | | AsnOrderLog.class, |
| | | asnOrderLogService, |
| | | "id", |
| | | DEFAULT_CURSOR_PAGE_SIZE, |
| | | null, |
| | | buildPageRowsUtils::userNameMap |
| | | )); |
| | | } |
| | | |
| | | @PreAuthorize("hasAuthority('manager:asnOrderLog:list')") |
| | |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.*; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.*; |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.fasterxml.jackson.annotation.JsonFormat; |
| | | import com.vincent.rsf.server.system.constant.DictTypeCode; |
| | | import com.vincent.rsf.server.system.entity.DictData; |
| | | import com.vincent.rsf.server.system.service.DictDataService; |
| | | import org.springframework.format.annotation.DateTimeFormat; |
| | | import com.baomidou.mybatisplus.annotation.TableLogic; |
| | | |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.Date; |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.Date; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableLogic; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | |
| | | @ApiModelProperty(value= "添加人员") |
| | | private Long createBy; |
| | | |
| | | @TableField(exist = false) |
| | | private String createBy$; |
| | | |
| | | /** |
| | | * 添加时间 |
| | | */ |
| | |
| | | */ |
| | | @ApiModelProperty(value= "修改人员") |
| | | private Long updateBy; |
| | | |
| | | @TableField(exist = false) |
| | | private String updateBy$; |
| | | |
| | | /** |
| | | * 修改时间 |
| | |
| | | } |
| | | } |
| | | |
| | | public String getCreateBy$(){ |
| | | UserService service = SpringUtils.getBean(UserService.class); |
| | | User user = service.getById(this.createBy); |
| | | if (!Cools.isEmpty(user)){ |
| | | return String.valueOf(user.getNickname()); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | public String getCreateTime$(){ |
| | | if (Cools.isEmpty(this.createTime)){ |
| | | return ""; |
| | | } |
| | | return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.createTime); |
| | | } |
| | | |
| | | public String getUpdateBy$(){ |
| | | UserService service = SpringUtils.getBean(UserService.class); |
| | | User user = service.getById(this.updateBy); |
| | | if (!Cools.isEmpty(user)){ |
| | | return String.valueOf(user.getNickname()); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | public String getUpdateTime$(){ |
| | |
| | | package com.vincent.rsf.server.manager.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.toolkit.Constants; |
| | | import com.vincent.rsf.server.manager.controller.dto.DashboardDto; |
| | | import com.vincent.rsf.server.manager.controller.dto.StockTransItemDto; |
| | | import com.vincent.rsf.server.manager.entity.StockStatistic; |
| | | import com.vincent.rsf.server.manager.entity.WkOrder; |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | import org.apache.ibatis.annotations.Param; |
| | | import org.springframework.stereotype.Repository; |
| | | |
| | | import java.util.List; |
| | |
| | | @Repository |
| | | public interface AsnOrderMapper extends BaseMapper<WkOrder> { |
| | | |
| | | DashboardDto getDashbord(@Param("type") String type, @Param("taskType") String taskType); |
| | | |
| | | List<StockTransItemDto> getStockTrand(@Param(Constants.WRAPPER) LambdaQueryWrapper<StockStatistic> queryWrapper); |
| | | List<StockTransItemDto> getStockTrand(); |
| | | } |
| | |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.vincent.rsf.framework.exception.CoolException; |
| | | import com.vincent.rsf.server.manager.entity.AsnOrderItemLog; |
| | | import com.vincent.rsf.server.manager.entity.AsnOrderLog; |
| | | import com.vincent.rsf.server.manager.entity.Matnr; |
| | | import com.vincent.rsf.server.manager.entity.WkOrder; |
| | | import com.vincent.rsf.server.manager.entity.WkOrderItem; |
| | | import com.vincent.rsf.server.manager.service.AsnOrderItemService; |
| | | import com.vincent.rsf.server.manager.service.AsnOrderService; |
| | | import com.vincent.rsf.server.manager.service.AsnOrderItemLogService; |
| | | import com.vincent.rsf.server.manager.service.AsnOrderLogService; |
| | | import com.vincent.rsf.server.manager.service.MatnrService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.commons.lang3.StringUtils; |
| | |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.Date; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.concurrent.atomic.AtomicBoolean; |
| | | |
| | | /** |
| | |
| | | private final AtomicBoolean running = new AtomicBoolean(false); |
| | | |
| | | @Autowired |
| | | private AsnOrderService asnOrderService; |
| | | private AsnOrderLogService asnOrderLogService; |
| | | @Autowired |
| | | private AsnOrderItemService asnOrderItemService; |
| | | private AsnOrderItemLogService asnOrderItemLogService; |
| | | @Autowired |
| | | private MatnrService matnrService; |
| | | |
| | |
| | | LocalDateTime nowTime = LocalDateTime.now(); |
| | | double totalQty = itemCountPerOrder * itemQty; |
| | | |
| | | List<WkOrder> orders = new ArrayList<>(orderCountPerRun); |
| | | List<AsnOrderLog> orders = new ArrayList<>(orderCountPerRun); |
| | | for (int i = 0; i < orderCountPerRun; i++) { |
| | | orders.add(buildOrder(now, nowTime, totalQty, i)); |
| | | } |
| | | if (!asnOrderService.saveBatch(orders, 200)) { |
| | | if (!asnOrderLogService.saveBatch(orders, 200)) { |
| | | throw new CoolException("ASN压测主单插入失败"); |
| | | } |
| | | |
| | | List<WkOrderItem> items = new ArrayList<>(orderCountPerRun * itemCountPerOrder); |
| | | List<AsnOrderLog> persistedOrders = asnOrderLogService.list(new LambdaQueryWrapper<AsnOrderLog>() |
| | | .in(AsnOrderLog::getCode, extractOrderCodes(orders))); |
| | | Map<String, AsnOrderLog> orderMap = new HashMap<>(persistedOrders.size()); |
| | | for (AsnOrderLog order : persistedOrders) { |
| | | orderMap.put(order.getCode(), order); |
| | | } |
| | | |
| | | List<AsnOrderItemLog> items = new ArrayList<>(orderCountPerRun * itemCountPerOrder); |
| | | for (int orderIndex = 0; orderIndex < orders.size(); orderIndex++) { |
| | | WkOrder order = orders.get(orderIndex); |
| | | AsnOrderLog order = orderMap.get(orders.get(orderIndex).getCode()); |
| | | if (order == null) { |
| | | throw new CoolException("ASN压测主单回查失败"); |
| | | } |
| | | for (int itemIndex = 0; itemIndex < itemCountPerOrder; itemIndex++) { |
| | | Matnr matnr = matnrs.get((orderIndex * itemCountPerOrder + itemIndex) % matnrs.size()); |
| | | items.add(buildOrderItem(order, matnr, now, orderIndex, itemIndex)); |
| | | } |
| | | } |
| | | if (!asnOrderItemService.saveBatch(items, 500)) { |
| | | if (!asnOrderItemLogService.saveBatch(items, 500)) { |
| | | throw new CoolException("ASN压测明细插入失败"); |
| | | } |
| | | |
| | |
| | | .last("limit " + needCount)); |
| | | } |
| | | |
| | | private WkOrder buildOrder(Date now, LocalDateTime nowTime, double totalQty, int sequence) { |
| | | private List<String> extractOrderCodes(List<AsnOrderLog> orders) { |
| | | List<String> codes = new ArrayList<>(orders.size()); |
| | | for (AsnOrderLog order : orders) { |
| | | codes.add(order.getCode()); |
| | | } |
| | | return codes; |
| | | } |
| | | |
| | | private AsnOrderLog buildOrder(Date now, LocalDateTime nowTime, double totalQty, int sequence) { |
| | | String suffix = String.format("%04d", sequence + 1); |
| | | String code = "erp" + nowTime.format(ORDER_CODE_FORMATTER) + suffix; |
| | | long serialNo = System.currentTimeMillis() * 1000 + sequence; |
| | | |
| | | return new WkOrder() |
| | | .setCode(code) |
| | | .setPoCode(code) |
| | | .setPoId(serialNo) |
| | | .setType(ORDER_TYPE) |
| | | .setWkType(ORDER_WORK_TYPE) |
| | | .setAnfme(totalQty) |
| | | .setQty(totalQty) |
| | | .setWorkQty(0.0) |
| | | .setCheckType(0) |
| | | .setRleStatus((short) 0) |
| | | .setNtyStatus(0) |
| | | .setExceStatus((short) 4) |
| | | .setStatus(1) |
| | | .setDeleted(0) |
| | | .setTenantId(TENANT_ID) |
| | | .setCreateBy(USER_ID) |
| | | .setCreateTime(now) |
| | | .setUpdateBy(USER_ID) |
| | | .setUpdateTime(now) |
| | | .setMemo(MEMO) |
| | | .setReportOnce(4) |
| | | .setBusinessTime(now) |
| | | .setStationId("1215") |
| | | .setOrderInternalCode(String.valueOf(serialNo)) |
| | | .setStockDirect("stockDirect") |
| | | .setCustomerId("custom1") |
| | | .setCustomerName("客户1") |
| | | .setSupplierId("gongys1") |
| | | .setSupplierName("供应商1") |
| | | .setStockOrgId("stockYH") |
| | | .setStockOrgName("浙江银湖箱包有限公司仓库") |
| | | .setPurchaseOrgId("yhcaigou") |
| | | .setPurchaseOrgName("浙江银湖箱包有限公司采购") |
| | | .setPurchaseUserId("caigouyuan1") |
| | | .setPurchaseUserName("采购员1") |
| | | .setPrdOrgId("prdYH") |
| | | .setPrdOrgName("浙江银湖箱包有限公司") |
| | | .setSaleOrgId("sale1") |
| | | .setSaleOrgName("生产组1") |
| | | .setSaleUserId("shengchanyuan1") |
| | | .setSaleUserName("生产员1") |
| | | .setVersion(0); |
| | | AsnOrderLog order = new AsnOrderLog(); |
| | | order.setCode(code); |
| | | order.setPoCode(code); |
| | | order.setPoId(serialNo); |
| | | order.setType(ORDER_TYPE); |
| | | order.setWkType(ORDER_WORK_TYPE); |
| | | order.setAnfme(totalQty); |
| | | order.setQty(totalQty); |
| | | order.setRleStatus((short) 0); |
| | | order.setNtyStatus((short) 0); |
| | | order.setExceStatus((short) 4); |
| | | order.setStatus(1); |
| | | order.setDeleted(0); |
| | | order.setTenantId(TENANT_ID); |
| | | order.setCreateBy(USER_ID); |
| | | order.setCreateTime(now); |
| | | order.setUpdateBy(USER_ID); |
| | | order.setUpdateTime(now); |
| | | order.setMemo(MEMO); |
| | | return order; |
| | | } |
| | | |
| | | private WkOrderItem buildOrderItem(WkOrder order, Matnr matnr, Date now, int orderIndex, int itemIndex) { |
| | | private AsnOrderItemLog buildOrderItem(AsnOrderLog order, Matnr matnr, Date now, int orderIndex, int itemIndex) { |
| | | String stockUnit = StringUtils.firstNonBlank(matnr.getStockUnit(), matnr.getPurUnit(), matnr.getUnit(), matnr.getBaseUnit()); |
| | | String purUnit = StringUtils.firstNonBlank(matnr.getPurUnit(), matnr.getUnit(), matnr.getStockUnit(), matnr.getBaseUnit()); |
| | | String baseUnit = StringUtils.firstNonBlank(matnr.getBaseUnit(), matnr.getUnit(), matnr.getStockUnit(), matnr.getPurUnit()); |
| | | String batchCode = "B" + new SimpleDateFormat("yyyyMMddHHmmss").format(now) |
| | | + String.format("%02d%02d", orderIndex + 1, itemIndex + 1); |
| | | String trackCode = "T" + System.currentTimeMillis() + String.format("%02d%02d", orderIndex + 1, itemIndex + 1); |
| | | |
| | | return new WkOrderItem() |
| | | .setOrderId(order.getId()) |
| | | .setOrderCode(order.getCode()) |
| | | return new AsnOrderItemLog() |
| | | .setLogId(order.getId()) |
| | | .setAsnId(order.getAsnId()) |
| | | .setAsnCode(order.getCode()) |
| | | .setPlatItemId("M" + (itemIndex + 1)) |
| | | .setPoCode(order.getPoCode()) |
| | | .setFieldsIndex(matnr.getFieldsIndex()) |
| | | .setMatnrId(matnr.getId()) |
| | | .setMatnrCode(matnr.getCode()) |
| | | .setMaktx(matnr.getName()) |
| | | .setSpec(matnr.getSpec()) |
| | | .setModel(matnr.getModel()) |
| | | .setAnfme(itemQty) |
| | | .setWorkQty(0.0) |
| | | .setPurQty(itemQty) |
| | | .setQty(itemQty) |
| | | .setStockUnit(stockUnit) |
| | | .setPurUnit(purUnit) |
| | | .setBatch(batchCode) |
| | | .setSplrBatch(batchCode) |
| | | .setSplrCode("gongys1") |
| | | .setSplrName("供应商1") |
| | | .setTrackCode(trackCode) |
| | | .setBarcode(trackCode) |
| | | .setProdTime(new SimpleDateFormat("yyyy-MM-dd").format(now)) |
| | | .setNtyStatus(0) |
| | | .setNtyStatus((short) 0) |
| | | .setStatus(1) |
| | | .setDeleted(0) |
| | | .setTenantId(TENANT_ID) |
| | |
| | | .setCreateTime(now) |
| | | .setUpdateBy(USER_ID) |
| | | .setUpdateTime(now) |
| | | .setMemo(MEMO) |
| | | .setBaseUnit(baseUnit) |
| | | .setUseOrgId(matnr.getUseOrgId()) |
| | | .setUseOrgName(matnr.getUseOrgName()) |
| | | .setErpClsId(matnr.getErpClsId()) |
| | | .setPriceUnitId(baseUnit); |
| | | .setMemo(MEMO); |
| | | } |
| | | } |
| | |
| | | import com.vincent.rsf.framework.exception.CoolException; |
| | | import com.vincent.rsf.server.api.entity.dto.PoItemsDto; |
| | | import com.vincent.rsf.server.api.service.ReportMsgService; |
| | | import com.vincent.rsf.server.common.service.RedisService; |
| | | import com.vincent.rsf.server.common.utils.DateUtils; |
| | | import com.vincent.rsf.server.manager.controller.dto.DashboardDto; |
| | | import com.vincent.rsf.server.manager.controller.dto.StockTrandDto; |
| | |
| | | import com.vincent.rsf.server.manager.entity.*; |
| | | import com.vincent.rsf.server.manager.enums.*; |
| | | import com.vincent.rsf.server.manager.mapper.AsnOrderMapper; |
| | | import com.vincent.rsf.server.manager.mapper.TaskLogMapper; |
| | | import com.vincent.rsf.server.manager.mapper.TaskMapper; |
| | | import com.vincent.rsf.server.manager.service.*; |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.vincent.rsf.server.system.constant.SerialRuleCode; |
| | | import com.vincent.rsf.server.system.mapper.SerialRuleMapper; |
| | | import com.vincent.rsf.server.system.utils.SerialRuleUtils; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.commons.lang3.StringUtils; |
| | | import org.springframework.beans.BeanUtils; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import jakarta.annotation.Resource; |
| | | import java.time.LocalDate; |
| | | import java.time.ZoneId; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.text.DateFormat; |
| | | import java.text.ParsePosition; |
| | | import java.text.SimpleDateFormat; |
| | |
| | | * @return |
| | | * @time 2025/3/7 08:02 |
| | | */ |
| | | @Slf4j |
| | | @Service("asnOrderService") |
| | | public class AsnOrderServiceImpl extends ServiceImpl<AsnOrderMapper, WkOrder> implements AsnOrderService { |
| | | |
| | | private static final String DASHBOARD_HEADER_CACHE_FLAG = "DASHBOARD_HEADER"; |
| | | private static final String DASHBOARD_HEADER_CACHE_FRESH_SUFFIX = "FRESH"; |
| | | private static final String DASHBOARD_HEADER_CACHE_STALE_SUFFIX = "STALE"; |
| | | private static final int DASHBOARD_HEADER_CACHE_FRESH_TTL_SECONDS = 300; |
| | | private static final int DASHBOARD_HEADER_CACHE_STALE_TTL_SECONDS = 86400; |
| | | private static final DateTimeFormatter DASHBOARD_CACHE_DATE_FORMATTER = DateTimeFormatter.BASIC_ISO_DATE; |
| | | |
| | | @Autowired |
| | | private ReportMsgService reportMsgService; |
| | |
| | | private PurchaseItemService purchaseItemService; |
| | | @Autowired |
| | | private TaskMapper taskMapper; |
| | | @Autowired |
| | | private TaskLogMapper taskLogMapper; |
| | | @Autowired |
| | | private RedisService redisService; |
| | | |
| | | @Override |
| | | public boolean notifyInspect(List<WkOrder> orders) { |
| | |
| | | */ |
| | | @Override |
| | | public R getDashbord() { |
| | | DashboardDto dto = new DashboardDto(); |
| | | //获取入库数量 |
| | | DashboardDto trandDto = this.baseMapper.getDashbord(OrderType.ORDER_IN.type, TaskType.TASK_TYPE_IN.type + ""); |
| | | dto.setInAnf(trandDto.getAnfme()).setTaskIn(trandDto.getRealAnfme()).setTotalIn(trandDto.getAnfme() + trandDto.getRealAnfme()); |
| | | |
| | | //获取出库单数量 |
| | | DashboardDto outTrand = this.baseMapper.getDashbord(OrderType.ORDER_OUT.type, TaskType.TASK_TYPE_OUT.type + ""); |
| | | dto.setOutAnf(outTrand.getAnfme()).setTaskOut(outTrand.getRealAnfme()).setTotalOut(outTrand.getAnfme() + outTrand.getRealAnfme()); |
| | | |
| | | //获取执行中任务数量 |
| | | List<Task> tasks = taskMapper.selectList(new LambdaQueryWrapper<>()); |
| | | if (!tasks.isEmpty()) { |
| | | dto.setTaskQty(tasks.size()); |
| | | String freshCacheKey = buildDashboardCacheKey(DASHBOARD_HEADER_CACHE_FRESH_SUFFIX); |
| | | DashboardDto freshSnapshot = getDashboardCache(freshCacheKey); |
| | | if (freshSnapshot != null) { |
| | | return R.ok().add(freshSnapshot); |
| | | } |
| | | return R.ok().add(dto); |
| | | String staleCacheKey = buildDashboardCacheKey(DASHBOARD_HEADER_CACHE_STALE_SUFFIX); |
| | | Exception dbException = null; |
| | | try { |
| | | DashboardDto snapshot = buildDashboardSnapshot(); |
| | | cacheDashboard(freshCacheKey, snapshot, DASHBOARD_HEADER_CACHE_FRESH_TTL_SECONDS); |
| | | cacheDashboard(staleCacheKey, snapshot, DASHBOARD_HEADER_CACHE_STALE_TTL_SECONDS); |
| | | return R.ok().add(snapshot); |
| | | } catch (Exception ex) { |
| | | dbException = ex; |
| | | log.warn("Load dashboard snapshot from database failed, fallback to stale cache. message={}", ex.getMessage(), ex); |
| | | } |
| | | |
| | | DashboardDto staleSnapshot = getDashboardCache(staleCacheKey); |
| | | if (staleSnapshot != null) { |
| | | return R.ok().add(staleSnapshot); |
| | | } |
| | | log.error("Load dashboard snapshot failed, returning empty snapshot.", dbException); |
| | | return R.ok().add(emptyDashboardSnapshot()); |
| | | } |
| | | |
| | | /** |
| | |
| | | @Override |
| | | public R getStockTrand() { |
| | | List<String> days = DateUtils.getLastMonthDays("yyyy-MM-dd"); |
| | | LambdaQueryWrapper<StockStatistic> queryWrapper = new LambdaQueryWrapper<StockStatistic>() |
| | | .in(StockStatistic::getTaskType, Arrays.asList(TaskType.TASK_TYPE_IN.type, TaskType.TASK_TYPE_OUT.type)); |
| | | List<StockTransItemDto> items = this.baseMapper.getStockTrand(queryWrapper); |
| | | List<StockTransItemDto> items = this.baseMapper.getStockTrand(); |
| | | if (items.isEmpty()) { |
| | | return R.ok(); |
| | | } |
| | |
| | | throw new CoolException("原单据删除失败!!"); |
| | | } |
| | | } |
| | | |
| | | private DashboardDto buildDashboardSnapshot() { |
| | | Date[] todayRange = buildTodayRange(); |
| | | Date todayStart = todayRange[0]; |
| | | Date tomorrowStart = todayRange[1]; |
| | | |
| | | int inAnf = safeToInt(this.count(new LambdaQueryWrapper<WkOrder>() |
| | | .eq(WkOrder::getType, OrderType.ORDER_IN.type) |
| | | .ge(WkOrder::getCreateTime, todayStart) |
| | | .lt(WkOrder::getCreateTime, tomorrowStart))); |
| | | int outAnf = safeToInt(this.count(new LambdaQueryWrapper<WkOrder>() |
| | | .eq(WkOrder::getType, OrderType.ORDER_OUT.type) |
| | | .ge(WkOrder::getCreateTime, todayStart) |
| | | .lt(WkOrder::getCreateTime, tomorrowStart))); |
| | | int taskIn = safeToInt(taskLogMapper.selectCount(new LambdaQueryWrapper<TaskLog>() |
| | | .eq(TaskLog::getTaskType, TaskType.TASK_TYPE_IN.type) |
| | | .ge(TaskLog::getCreateTime, todayStart) |
| | | .lt(TaskLog::getCreateTime, tomorrowStart))); |
| | | int taskOut = safeToInt(taskLogMapper.selectCount(new LambdaQueryWrapper<TaskLog>() |
| | | .eq(TaskLog::getTaskType, TaskType.TASK_TYPE_OUT.type) |
| | | .ge(TaskLog::getCreateTime, todayStart) |
| | | .lt(TaskLog::getCreateTime, tomorrowStart))); |
| | | int taskQty = safeToInt(taskMapper.selectCount(new LambdaQueryWrapper<Task>())); |
| | | |
| | | return new DashboardDto() |
| | | .setInAnf(inAnf) |
| | | .setOutAnf(outAnf) |
| | | .setTaskIn(taskIn) |
| | | .setTaskOut(taskOut) |
| | | .setTaskQty(taskQty) |
| | | .setTotalIn(inAnf + taskIn) |
| | | .setTotalOut(outAnf + taskOut); |
| | | } |
| | | |
| | | private DashboardDto getDashboardCache(String cacheKey) { |
| | | try { |
| | | String cacheValue = redisService.getValue(DASHBOARD_HEADER_CACHE_FLAG, cacheKey); |
| | | if (StringUtils.isBlank(cacheValue)) { |
| | | return null; |
| | | } |
| | | return JSONObject.parseObject(cacheValue, DashboardDto.class); |
| | | } catch (Exception ex) { |
| | | log.warn("Read dashboard cache failed, key={}, message={}", cacheKey, ex.getMessage(), ex); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private void cacheDashboard(String cacheKey, DashboardDto dto, int ttlSeconds) { |
| | | try { |
| | | redisService.setValue(DASHBOARD_HEADER_CACHE_FLAG, cacheKey, JSONObject.toJSONString(dto), ttlSeconds); |
| | | } catch (Exception ex) { |
| | | log.warn("Write dashboard cache failed, key={}, message={}", cacheKey, ex.getMessage(), ex); |
| | | } |
| | | } |
| | | |
| | | private String buildDashboardCacheKey(String suffix) { |
| | | String dateBucket = LocalDate.now().format(DASHBOARD_CACHE_DATE_FORMATTER); |
| | | return dateBucket + "." + suffix; |
| | | } |
| | | |
| | | private Date[] buildTodayRange() { |
| | | ZoneId zoneId = ZoneId.systemDefault(); |
| | | LocalDate today = LocalDate.now(zoneId); |
| | | Date todayStart = Date.from(today.atStartOfDay(zoneId).toInstant()); |
| | | Date tomorrowStart = Date.from(today.plusDays(1).atStartOfDay(zoneId).toInstant()); |
| | | return new Date[]{todayStart, tomorrowStart}; |
| | | } |
| | | |
| | | private DashboardDto emptyDashboardSnapshot() { |
| | | return new DashboardDto() |
| | | .setInAnf(0) |
| | | .setOutAnf(0) |
| | | .setTaskIn(0) |
| | | .setTaskOut(0) |
| | | .setTaskQty(0) |
| | | .setTotalIn(0) |
| | | .setTotalOut(0); |
| | | } |
| | | |
| | | private int safeToInt(Long count) { |
| | | return count == null ? 0 : count.intValue(); |
| | | } |
| | | } |
| | |
| | | package com.vincent.rsf.server.system.controller; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.baomidou.mybatisplus.core.metadata.OrderItem; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.baomidou.mybatisplus.extension.service.IService; |
| | | import com.vincent.rsf.common.utils.Utils; |
| | | import com.vincent.rsf.framework.common.Cools; |
| | | import com.vincent.rsf.server.common.domain.BaseParam; |
| | | import com.vincent.rsf.server.common.domain.CursorPageParam; |
| | | import com.vincent.rsf.server.common.domain.CursorPageResult; |
| | | import com.vincent.rsf.server.common.domain.PageParam; |
| | | import com.vincent.rsf.server.system.entity.User; |
| | | import org.springframework.security.core.Authentication; |
| | | import org.springframework.security.core.GrantedAuthority; |
| | | import org.springframework.security.core.context.SecurityContextHolder; |
| | | |
| | | import java.lang.reflect.Field; |
| | | import java.util.ArrayList; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.function.Consumer; |
| | | |
| | | /** |
| | | * Created by vincent on 1/30/2024 |
| | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * 通用游标分页实现。 |
| | | * |
| | | * <p>这个方法的目标不是替代所有分页,而是把“单字段、倒序、向后翻页”的游标分页 |
| | | * 收敛成一套统一实现,避免各个 controller 重复写下面这些样板逻辑:</p> |
| | | * <ul> |
| | | * <li>buildParam</li> |
| | | * <li>忽略前端传入的 orderBy</li> |
| | | * <li>buildWrapper(true) 构建通用筛选</li> |
| | | * <li>cursor 条件</li> |
| | | * <li>按固定字段倒序</li> |
| | | * <li>多查一条判断 hasNext</li> |
| | | * <li>截断结果并生成 nextCursor</li> |
| | | * </ul> |
| | | * |
| | | * <p>适用前提:</p> |
| | | * <ul> |
| | | * <li>游标字段是单一字段</li> |
| | | * <li>游标字段的值稳定、可比较,并且能映射成 Long</li> |
| | | * <li>分页方向固定为“按该字段倒序,向更小的值翻页”</li> |
| | | * </ul> |
| | | * |
| | | * <p>参数说明:</p> |
| | | * <ul> |
| | | * <li>{@code map}:原始请求参数</li> |
| | | * <li>{@code paramClass}:游标参数类型,通常传 {@link CursorPageParam}</li> |
| | | * <li>{@code entityClass}:实体类,用于 PageParam 条件构建和 condition 模糊查询</li> |
| | | * <li>{@code service}:MyBatis-Plus 的 IService,负责执行 page 查询</li> |
| | | * <li>{@code cursorField}:实体字段名,不是数据库列名,例如传 {@code id}</li> |
| | | * <li>{@code defaultPageSize}:当前接口的默认分页大小</li> |
| | | * <li>{@code wrapperConsumer}:可选的额外 where 条件扩展钩子</li> |
| | | * <li>{@code recordsConsumer}:可选的结果后处理钩子,例如补充 createBy$/updateBy$</li> |
| | | * </ul> |
| | | */ |
| | | protected <T, U extends CursorPageParam> CursorPageResult<T> cursorPage( |
| | | Map<String, Object> map, |
| | | Class<U> paramClass, |
| | | Class<T> entityClass, |
| | | IService<T> service, |
| | | String cursorField, |
| | | int defaultPageSize, |
| | | Consumer<QueryWrapper<T>> wrapperConsumer, |
| | | Consumer<List<T>> recordsConsumer |
| | | ) { |
| | | // 允许 controller 传 null,内部统一兜底成空 map, |
| | | // 这样 buildParam 不需要每个调用方自己先判空。 |
| | | U baseParam = buildParam(map == null ? new HashMap<>() : map, paramClass); |
| | | |
| | | // 游标分页不允许客户端自定义排序, |
| | | // 否则“上一页最后一条作为下一页游标”的前提会被破坏。 |
| | | baseParam.setOrderBy(null); |
| | | |
| | | // pageSize 允许从请求里带入,但非法值(null、0、负数)统一回退到接口默认值。 |
| | | int pageSize = resolveCursorPageSize(baseParam.getPageSize(), defaultPageSize); |
| | | |
| | | // controller 传的是实体字段名,例如 "id" / "poId", |
| | | // 这里统一转成数据库列名并补反引号,避免每个业务自己手写 SQL 片段。 |
| | | String cursorColumn = resolveCursorColumn(cursorField); |
| | | |
| | | // 先复用系统现有的 PageParam + buildWrapper(true) 机制, |
| | | // 保留原来的条件解析、时间范围、condition 模糊搜索等能力。 |
| | | PageParam<T, U> pageParam = new PageParam<>(baseParam, entityClass); |
| | | QueryWrapper<T> wrapper = pageParam.buildWrapper(true); |
| | | |
| | | // 给业务预留额外 where 条件的扩展点; |
| | | // 如果某个接口除了通用筛选外,还要拼接额外限制,可以在这里补。 |
| | | if (wrapperConsumer != null) { |
| | | wrapperConsumer.accept(wrapper); |
| | | } |
| | | |
| | | // 游标分页的核心条件: |
| | | // 当前约定是“按 cursorField 倒序查看更旧的数据”,所以条件固定为 < cursor。 |
| | | if (baseParam.getCursor() != null) { |
| | | wrapper.lt(cursorColumn, baseParam.getCursor()); |
| | | } |
| | | |
| | | // 强制按游标字段倒序排序,保证每一页的数据顺序稳定。 |
| | | wrapper.orderByDesc(cursorColumn); |
| | | |
| | | // 多查一条是游标分页判断 hasNext 的常见做法: |
| | | // 实际要 20 条,就查 21 条;多出来那一条只用来判断是否还有下一页。 |
| | | Page<T> queryPage = new Page<>(1L, pageSize + 1L, false); |
| | | List<T> records = service.page(queryPage, wrapper).getRecords(); |
| | | List<T> pageRecords = Cools.isEmpty(records) ? new ArrayList<>() : new ArrayList<>(records); |
| | | |
| | | // 如果查出来的数量大于 pageSize,说明至少还有下一页。 |
| | | boolean hasNext = pageRecords.size() > pageSize; |
| | | if (hasNext) { |
| | | // 只把真正需要返回给前端的 pageSize 条数据留在当前页。 |
| | | pageRecords = new ArrayList<>(pageRecords.subList(0, pageSize)); |
| | | } |
| | | |
| | | // 给业务侧一个“结果出库前处理”的机会。 |
| | | // 典型场景是批量补充用户名、字典文本、缓存字段等, |
| | | // 这样公共分页逻辑不关心业务细节,但业务也不需要回到 controller 自己重写分页。 |
| | | if (recordsConsumer != null && !Cools.isEmpty(pageRecords)) { |
| | | recordsConsumer.accept(pageRecords); |
| | | } |
| | | |
| | | CursorPageResult<T> result = new CursorPageResult<>(); |
| | | result.setRecords(pageRecords); |
| | | result.setPageSize(pageSize); |
| | | result.setHasNext(hasNext); |
| | | // nextCursor 只有在还有下一页时才有意义; |
| | | // 约定取“当前页最后一条记录”的游标字段值。 |
| | | result.setNextCursor(hasNext ? extractCursorValue(pageRecords, cursorField) : null); |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 统一解析当前接口实际使用的 pageSize。 |
| | | * |
| | | * <p>只要前端没传、传了 0、或者传了负数,就回退到 controller 传入的默认值。</p> |
| | | */ |
| | | private int resolveCursorPageSize(Integer pageSize, int defaultPageSize) { |
| | | if (pageSize == null || pageSize <= 0) { |
| | | return defaultPageSize; |
| | | } |
| | | return pageSize; |
| | | } |
| | | |
| | | /** |
| | | * 把实体字段名转换成数据库列名。 |
| | | * |
| | | * <p>例如:</p> |
| | | * <ul> |
| | | * <li>{@code id -> `id`}</li> |
| | | * <li>{@code poId -> `po_id`}</li> |
| | | * </ul> |
| | | * |
| | | * <p>这样 controller 调用时只需要关心 Java 字段名,不需要自己拼 SQL。</p> |
| | | */ |
| | | private String resolveCursorColumn(String cursorField) { |
| | | return "`" + Utils.toSymbolCase(cursorField, '_') + "`"; |
| | | } |
| | | |
| | | /** |
| | | * 从当前页最后一条记录中提取 nextCursor。 |
| | | * |
| | | * <p>这里使用反射而不是额外定义接口,目的是降低接入成本: |
| | | * 只要实体里存在同名字段,就能直接复用通用方法。</p> |
| | | * |
| | | * <p>支持的字段值类型:</p> |
| | | * <ul> |
| | | * <li>{@link Long}</li> |
| | | * <li>其他 {@link Number}</li> |
| | | * <li>可转成 Long 的字符串</li> |
| | | * </ul> |
| | | * |
| | | * <p>如果字段不存在、为空、或无法转成 Long,则返回 null。</p> |
| | | */ |
| | | private <T> Long extractCursorValue(List<T> records, String cursorField) { |
| | | if (Cools.isEmpty(records)) { |
| | | return null; |
| | | } |
| | | T lastRecord = records.get(records.size() - 1); |
| | | if (lastRecord == null || Cools.isEmpty(cursorField)) { |
| | | return null; |
| | | } |
| | | Field field = Cools.getField(lastRecord.getClass(), cursorField); |
| | | if (field == null) { |
| | | return null; |
| | | } |
| | | boolean accessible = field.isAccessible(); |
| | | try { |
| | | field.setAccessible(true); |
| | | Object value = field.get(lastRecord); |
| | | if (value instanceof Long) { |
| | | return (Long) value; |
| | | } |
| | | if (value instanceof Number) { |
| | | return ((Number) value).longValue(); |
| | | } |
| | | if (value instanceof String && !((String) value).trim().isEmpty()) { |
| | | return Long.parseLong(((String) value).trim()); |
| | | } |
| | | } catch (IllegalAccessException | NumberFormatException ignored) { |
| | | return null; |
| | | } finally { |
| | | field.setAccessible(accessible); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | } |
| | | |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
| | | <mapper namespace="com.vincent.rsf.server.manager.mapper.AsnOrderMapper"> |
| | | <select id="getDashbord" resultType="com.vincent.rsf.server.manager.controller.dto.DashboardDto"> |
| | | SELECT |
| | | ( SELECT COUNT( 1 ) FROM man_asn_order WHERE DATE(create_time) = CURRENT_DATE() AND `type` = #{type} ) AS anfme, |
| | | COUNT( id ) AS real_anfme |
| | | FROM |
| | | man_task_log |
| | | WHERE |
| | | DATE(create_time) = CURRENT_DATE() |
| | | AND task_type = #{taskType} |
| | | </select> |
| | | |
| | | <select id="getStockTrand" resultType="com.vincent.rsf.server.manager.controller.dto.StockTransItemDto"> |
| | | SELECT * FROM |
| | | ( |
| | |
| | | FROM |
| | | view_stock_statistic |
| | | WHERE |
| | | task_type IN (1, 101) |
| | | AND |
| | | `day_time` BETWEEN ( CURDATE() - INTERVAL 1 MONTH ) |
| | | AND CURDATE() |
| | | GROUP BY |
| | | `day_time`, task_type,id |
| | | ) t |
| | | ${ew.customSqlSegment} |
| | | </select> |
| | | </mapper> |