已添加38个文件
已删除8个文件
已修改23个文件
| | |
| | | # åºç¨æ é¢ |
| | | VITE_APP_TITLE=å·¥ä¸AIæºé å¹³å° |
| | | VITE_APP_TITLE=æ±èåè´¸ |
| | | |
| | | # åºç¨å½å空é´ï¼ç¨äºç¼åãstoreçåè½çåç¼ï¼ç¡®ä¿é离 |
| | | VITE_APP_NAMESPACE=yudao-vben-antd |
| | |
| | | # 端å£å· |
| | | VITE_PORT=5666 |
| | | |
| | | # åºç¨æ é¢ |
| | | VITE_APP_TITLE=æ±èåè´¸ |
| | | |
| | | # å½åå
¬å¸æ è¯ï¼å³å®åºç¨åç§° / Logo / 徿 ï¼è§ company.config.tsï¼ |
| | | VITE_COMPANY=JSSM |
| | | |
| | | VITE_BASE=/ |
| | | |
| | | # 请æ±è·¯å¾ |
| | |
| | | VITE_APP_DEFAULT_PASSWORD=admin123 |
| | | |
| | | # 产åè®¾è®¡æ¨¡åæ¬å° Mockï¼æ åç«¯æ¶æ¼ç¤ºï¼ |
| | | VITE_PD_MOCK=true |
| | | VITE_PD_MOCK=true |
| | |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://36.213.150.237:9006/admin-api', |
| | | }, |
| | | JSSM: { |
| | | name: 'æ±èåè´¸', |
| | | logo: '/logos/JSSM.svg', |
| | | favicon: '/logos/JSSM.svg', |
| | | apiUrl: '', |
| | | }, |
| | | }; |
| | |
| | | "@vueuse/core": "catalog:", |
| | | "@vueuse/integrations": "catalog:", |
| | | "ant-design-vue": "catalog:", |
| | | "axios": "catalog:", |
| | | "bpmn-js": "catalog:", |
| | | "bpmn-js-properties-panel": "catalog:", |
| | | "bpmn-js-token-simulation": "catalog:", |
| | |
| | | ant-design-vue: |
| | | specifier: 'catalog:' |
| | | version: 4.2.6(vue@3.5.38(typescript@6.0.3)) |
| | | axios: |
| | | specifier: 'catalog:' |
| | | version: 1.18.1 |
| | | bpmn-js: |
| | | specifier: 'catalog:' |
| | | version: 18.18.0 |
| | |
| | | '@lezer/common@1.5.2': |
| | | resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} |
| | | |
| | | '@lezer/highlight@1.2.3': |
| | | resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} |
| | | '@lezer/highlight@1.2.4': |
| | | resolution: {integrity: sha512-4UeRVFO0lsBaM7GA32zOqbmqNPme9jedo8Ey+5c5656MU3MLitMLIA8aCZTYjUMEpEsuSeXEm78Uea6Iq3bvSg==} |
| | | |
| | | '@lezer/json@1.0.3': |
| | | resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} |
| | |
| | | strnum@1.0.5: |
| | | resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} |
| | | |
| | | style-mod@4.1.3: |
| | | resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} |
| | | style-mod@4.1.4: |
| | | resolution: {integrity: sha512-XXWIQt633/EpAFx8aZDOTjBzrCaGmhvEQlQo6MVPfa2OzO2cWo+4hV9h+6UkHYlXGfy+ODXKUdP7Pthmcu5ATw==} |
| | | |
| | | style-search@0.1.0: |
| | | resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==} |
| | |
| | | resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} |
| | | engines: {node: '>= 0.4'} |
| | | |
| | | typed-array-byte-offset@1.0.4: |
| | | resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} |
| | | typed-array-byte-offset@1.0.5: |
| | | resolution: {integrity: sha512-0FHJvLPqZ7KJzp17O13jfsAjsqazgrxBu2zEK95PmUz8lv2+GjRuxUInCr2Rk9Dms3ihN21zJ929ZO43yJ95QQ==} |
| | | engines: {node: '>= 0.4'} |
| | | |
| | | typed-array-length@1.0.8: |
| | |
| | | dependencies: |
| | | '@codemirror/language': 6.12.3 |
| | | '@codemirror/view': 6.43.1 |
| | | '@lezer/highlight': 1.2.3 |
| | | '@lezer/highlight': 1.2.4 |
| | | |
| | | '@bpmn-io/diagram-js-ui@0.2.4': |
| | | dependencies: |
| | |
| | | '@codemirror/lint': 6.9.7 |
| | | '@codemirror/state': 6.6.0 |
| | | '@codemirror/view': 6.43.1 |
| | | '@lezer/highlight': 1.2.3 |
| | | '@lezer/highlight': 1.2.4 |
| | | min-dom: 5.3.0 |
| | | mitt: 3.0.1 |
| | | |
| | |
| | | |
| | | '@bpmn-io/lezer-feel@2.5.0': |
| | | dependencies: |
| | | '@lezer/highlight': 1.2.3 |
| | | '@lezer/highlight': 1.2.4 |
| | | '@lezer/lr': 1.4.10 |
| | | min-dash: 5.0.0 |
| | | |
| | | '@bpmn-io/lezer-feel@3.0.1': |
| | | dependencies: |
| | | '@lezer/highlight': 1.2.3 |
| | | '@lezer/highlight': 1.2.4 |
| | | '@lezer/lr': 1.4.10 |
| | | min-dash: 5.0.0 |
| | | |
| | |
| | | '@codemirror/state': 6.6.0 |
| | | '@codemirror/view': 6.43.1 |
| | | '@lezer/common': 1.5.2 |
| | | '@lezer/highlight': 1.2.3 |
| | | '@lezer/highlight': 1.2.4 |
| | | '@lezer/lr': 1.4.10 |
| | | style-mod: 4.1.3 |
| | | style-mod: 4.1.4 |
| | | |
| | | '@codemirror/lint@6.9.7': |
| | | dependencies: |
| | |
| | | dependencies: |
| | | '@codemirror/state': 6.6.0 |
| | | crelt: 1.0.7 |
| | | style-mod: 4.1.3 |
| | | style-mod: 4.1.4 |
| | | w3c-keyname: 2.2.8 |
| | | |
| | | '@commitlint/cli@19.8.0(@types/node@25.9.4)(typescript@6.0.3)': |
| | |
| | | |
| | | '@lezer/common@1.5.2': {} |
| | | |
| | | '@lezer/highlight@1.2.3': |
| | | '@lezer/highlight@1.2.4': |
| | | dependencies: |
| | | '@lezer/common': 1.5.2 |
| | | |
| | | '@lezer/json@1.0.3': |
| | | dependencies: |
| | | '@lezer/common': 1.5.2 |
| | | '@lezer/highlight': 1.2.3 |
| | | '@lezer/highlight': 1.2.4 |
| | | '@lezer/lr': 1.4.10 |
| | | |
| | | '@lezer/lr@1.4.10': |
| | |
| | | '@lezer/markdown@1.6.4': |
| | | dependencies: |
| | | '@lezer/common': 1.5.2 |
| | | '@lezer/highlight': 1.2.3 |
| | | '@lezer/highlight': 1.2.4 |
| | | |
| | | '@livekit/mutex@1.1.1': {} |
| | | |
| | |
| | | string.prototype.trimstart: 1.0.8 |
| | | typed-array-buffer: 1.0.3 |
| | | typed-array-byte-length: 1.0.3 |
| | | typed-array-byte-offset: 1.0.4 |
| | | typed-array-byte-offset: 1.0.5 |
| | | typed-array-length: 1.0.8 |
| | | unbox-primitive: 1.1.0 |
| | | which-typed-array: 1.1.22 |
| | |
| | | '@codemirror/state': 6.6.0 |
| | | '@codemirror/view': 6.43.1 |
| | | '@lezer/common': 1.5.2 |
| | | '@lezer/highlight': 1.2.3 |
| | | '@lezer/highlight': 1.2.4 |
| | | '@lezer/lr': 1.4.10 |
| | | '@lezer/markdown': 1.6.4 |
| | | min-dom: 5.3.0 |
| | |
| | | |
| | | strnum@1.0.5: {} |
| | | |
| | | style-mod@4.1.3: {} |
| | | style-mod@4.1.4: {} |
| | | |
| | | style-search@0.1.0: {} |
| | | |
| | |
| | | has-proto: 1.2.0 |
| | | is-typed-array: 1.1.15 |
| | | |
| | | typed-array-byte-offset@1.0.4: |
| | | typed-array-byte-offset@1.0.5: |
| | | dependencies: |
| | | available-typed-arrays: 1.0.7 |
| | | call-bind: 1.0.9 |
| | | for-each: 0.3.5 |
| | | gopd: 1.2.0 |
| | | has-proto: 1.2.0 |
| | | is-typed-array: 1.1.15 |
| | | reflect.getprototypeof: 1.0.10 |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="æ±èåè´¸"> |
| | | <defs> |
| | | <!-- èé主è²ï¼å·¦ä¸è§ç¥äº®ï¼å³ä¸è§æ²ä¸å»ï¼å½¢æä½ç§¯æ --> |
| | | <linearGradient id="jssmNavy" x1="0" y1="0" x2="0.85" y2="1"> |
| | | <stop offset="0%" stop-color="#1E4571"/> |
| | | <stop offset="52%" stop-color="#123156"/> |
| | | <stop offset="100%" stop-color="#0A1D35"/> |
| | | </linearGradient> |
| | | <!-- éè²ï¼ä¸äº®ä¸æ·±ï¼æ¨¡æéå±å
æ³½ --> |
| | | <linearGradient id="jssmGold" x1="0" y1="0" x2="0" y2="1"> |
| | | <stop offset="0%" stop-color="#EBD07E"/> |
| | | <stop offset="45%" stop-color="#D4AF37"/> |
| | | <stop offset="100%" stop-color="#B8901F"/> |
| | | </linearGradient> |
| | | </defs> |
| | | |
| | | <!-- ç¾å½¢å¾½æ ä¸»ä½ --> |
| | | <path d="M8 14 A6 6 0 0 1 14 8 H50 A6 6 0 0 1 56 14 V32 C56 46 45.5 55.5 32 60 C18.5 55.5 8 46 8 32 Z" |
| | | fill="url(#jssmNavy)"/> |
| | | <!-- å¤åéè¾¹ï¼æ·±è²èæ¯ä¸é å®å¾åè½®å» --> |
| | | <path d="M8 14 A6 6 0 0 1 14 8 H50 A6 6 0 0 1 56 14 V32 C56 46 45.5 55.5 32 60 C18.5 55.5 8 46 8 32 Z" |
| | | fill="none" stroke="url(#jssmGold)" stroke-width="1.5" opacity="0.9"/> |
| | | <!-- å
åé线ï¼å¢å å¾½ç« ç屿¬¡ --> |
| | | <path d="M12.5 15 A3.5 3.5 0 0 1 16 11.5 H48 A3.5 3.5 0 0 1 51.5 15 V32 C51.5 44 42.5 52.5 32 56.5 C21.5 52.5 12.5 44 12.5 32 Z" |
| | | fill="none" stroke="url(#jssmGold)" stroke-width="1" opacity="0.55"/> |
| | | |
| | | <!-- é¡¶é¨å¦æ¯ä¹æ --> |
| | | <path d="M32 15.6 L33.9 19 L32 22.4 L30.1 19 Z" fill="url(#jssmGold)"/> |
| | | |
| | | <!-- å±å¼ç书æ¬ï¼å·¦é¡µ --> |
| | | <path d="M31.2 29.5 C27.4 26.1 22.2 24.6 17.8 25.5 V41.4 C22.2 40.5 27.4 42 31.2 45.4 Z" |
| | | fill="url(#jssmGold)"/> |
| | | <!-- å±å¼ç书æ¬ï¼å³é¡µï¼ç¥æï¼æå¼å·¦å³å±æ¬¡ï¼ --> |
| | | <path d="M32.8 29.5 C36.6 26.1 41.8 24.6 46.2 25.5 V41.4 C41.8 40.5 36.6 42 32.8 45.4 Z" |
| | | fill="url(#jssmGold)" opacity="0.78"/> |
| | | <!-- 书è --> |
| | | <path d="M32 29.5 V45.4" stroke="#0A1D35" stroke-width="1.6" stroke-linecap="round" opacity="0.55"/> |
| | | </svg> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 900" preserveAspectRatio="xMidYMid slice" role="img" aria-label="妿¯æ ¡å夿¯"> |
| | | <defs> |
| | | <!-- å¤ç©ºï¼ä¸æ·±ä¸æµ
çèé --> |
| | | <linearGradient id="sky" x1="0" y1="0" x2="0.25" y2="1"> |
| | | <stop offset="0%" stop-color="#08182C"/> |
| | | <stop offset="42%" stop-color="#0F2440"/> |
| | | <stop offset="72%" stop-color="#14375F"/> |
| | | <stop offset="100%" stop-color="#0B1C33"/> |
| | | </linearGradient> |
| | | |
| | | <!-- æå
å
æ --> |
| | | <radialGradient id="moonGlow" cx="0.5" cy="0.5" r="0.5"> |
| | | <stop offset="0%" stop-color="#EBD07E" stop-opacity="0.30"/> |
| | | <stop offset="45%" stop-color="#D4AF37" stop-opacity="0.10"/> |
| | | <stop offset="100%" stop-color="#D4AF37" stop-opacity="0"/> |
| | | </radialGradient> |
| | | |
| | | <!-- 建çåå
é¢ï¼å·¦äº®å³æ --> |
| | | <linearGradient id="facade" x1="0" y1="0" x2="1" y2="0.35"> |
| | | <stop offset="0%" stop-color="#1E4571"/> |
| | | <stop offset="55%" stop-color="#16375C"/> |
| | | <stop offset="100%" stop-color="#0E2743"/> |
| | | </linearGradient> |
| | | |
| | | <!-- 穹顶 --> |
| | | <linearGradient id="dome" x1="0.15" y1="0" x2="0.85" y2="1"> |
| | | <stop offset="0%" stop-color="#2A5B8C"/> |
| | | <stop offset="60%" stop-color="#17395E"/> |
| | | <stop offset="100%" stop-color="#0D2340"/> |
| | | </linearGradient> |
| | | |
| | | <!-- éè²çªå
--> |
| | | <linearGradient id="winLight" x1="0" y1="0" x2="0" y2="1"> |
| | | <stop offset="0%" stop-color="#EBD07E"/> |
| | | <stop offset="100%" stop-color="#C9A227"/> |
| | | </linearGradient> |
| | | |
| | | <!-- æ°´é¢ --> |
| | | <linearGradient id="pool" x1="0" y1="0" x2="0" y2="1"> |
| | | <stop offset="0%" stop-color="#0C2036"/> |
| | | <stop offset="100%" stop-color="#061320"/> |
| | | </linearGradient> |
| | | |
| | | <!-- æ°´é¢éè²åå½± --> |
| | | <linearGradient id="reflect" x1="0" y1="0" x2="0" y2="1"> |
| | | <stop offset="0%" stop-color="#D4AF37" stop-opacity="0.22"/> |
| | | <stop offset="100%" stop-color="#D4AF37" stop-opacity="0"/> |
| | | </linearGradient> |
| | | |
| | | <!-- åºé¨åæï¼ç»æåçåºå¯è¯»åºå --> |
| | | <linearGradient id="vignette" x1="0" y1="0" x2="0" y2="1"> |
| | | <stop offset="0%" stop-color="#08182C" stop-opacity="0"/> |
| | | <stop offset="62%" stop-color="#08182C" stop-opacity="0.35"/> |
| | | <stop offset="100%" stop-color="#08182C" stop-opacity="0.85"/> |
| | | </linearGradient> |
| | | </defs> |
| | | |
| | | <!-- ===== å¤ç©º ===== --> |
| | | <rect width="900" height="900" fill="url(#sky)"/> |
| | | |
| | | <!-- æäº®ä¸å
æ --> |
| | | <circle cx="686" cy="176" r="210" fill="url(#moonGlow)"/> |
| | | <circle cx="686" cy="176" r="46" fill="#EBD07E" opacity="0.92"/> |
| | | <circle cx="686" cy="176" r="46" fill="none" stroke="#F5E7C0" stroke-width="1" opacity="0.5"/> |
| | | <!-- æé¢æé¨ï¼è®©å®ä¸æ¯çº¯è²å --> |
| | | <circle cx="672" cy="164" r="38" fill="#D4AF37" opacity="0.18"/> |
| | | <circle cx="700" cy="188" r="14" fill="#08182C" opacity="0.12"/> |
| | | <circle cx="672" cy="196" r="9" fill="#08182C" opacity="0.10"/> |
| | | |
| | | <!-- æç¹ --> |
| | | <g fill="#EBD07E"> |
| | | <circle cx="120" cy="96" r="1.8" opacity="0.55"/> |
| | | <circle cx="238" cy="152" r="1.3" opacity="0.42"/> |
| | | <circle cx="352" cy="82" r="2.1" opacity="0.6"/> |
| | | <circle cx="470" cy="134" r="1.4" opacity="0.4"/> |
| | | <circle cx="560" cy="72" r="1.7" opacity="0.5"/> |
| | | <circle cx="800" cy="118" r="1.5" opacity="0.45"/> |
| | | <circle cx="176" cy="228" r="1.2" opacity="0.32"/> |
| | | <circle cx="420" cy="206" r="1.6" opacity="0.44"/> |
| | | <circle cx="742" cy="268" r="1.3" opacity="0.34"/> |
| | | <circle cx="86" cy="330" r="1.5" opacity="0.3"/> |
| | | <circle cx="598" cy="184" r="1.1" opacity="0.3"/> |
| | | <circle cx="300" cy="286" r="1.2" opacity="0.28"/> |
| | | </g> |
| | | |
| | | <!-- ===== è¿å±± ===== --> |
| | | <path d="M0 556 L118 484 L214 534 L322 452 L430 528 L548 470 L662 540 L784 486 L900 552 L900 900 L0 900 Z" |
| | | fill="#0B1E36" opacity="0.75"/> |
| | | |
| | | <!-- ===== è¿æ¯æ¥¼ç¾¤ ===== --> |
| | | <g fill="#0D2440" opacity="0.85"> |
| | | <rect x="42" y="520" width="46" height="72"/> |
| | | <rect x="98" y="546" width="34" height="46"/> |
| | | <rect x="748" y="530" width="42" height="62"/> |
| | | <rect x="800" y="556" width="30" height="36"/> |
| | | </g> |
| | | |
| | | <!-- ===== 主楼ï¼å°é¶ ===== --> |
| | | <g> |
| | | <rect x="238" y="742" width="424" height="13" fill="#132E4E"/> |
| | | <rect x="256" y="729" width="388" height="13" fill="#17395E"/> |
| | | <rect x="274" y="716" width="352" height="13" fill="#1B4168"/> |
| | | </g> |
| | | |
| | | <!-- ===== ä¸»æ¥¼ï¼æ±å» ===== --> |
| | | <rect x="286" y="560" width="328" height="156" fill="url(#facade)"/> |
| | | |
| | | <!-- ===== å±é¡¶å±±è±ï¼ä¸è§æ¥£ï¼ ===== --> |
| | | <path d="M286 560 L450 470 L614 560 Z" fill="#17395E"/> |
| | | <path d="M286 560 L450 470 L614 560 Z" fill="none" stroke="#D4AF37" stroke-width="2" opacity="0.55"/> |
| | | <rect x="272" y="556" width="356" height="12" fill="#1B4168"/> |
| | | |
| | | <!-- å±±è±å
çåå¾½ --> |
| | | <circle cx="450" cy="528" r="24" fill="#0E2743" stroke="#D4AF37" stroke-width="1.6" opacity="0.9"/> |
| | | <circle cx="450" cy="528" r="13" fill="none" stroke="#D4AF37" stroke-width="1.2" opacity="0.65"/> |
| | | <circle cx="450" cy="528" r="4.5" fill="#D4AF37" opacity="0.85"/> |
| | | |
| | | <!-- ===== 穹顶 ===== --> |
| | | <path d="M450 470 A62 62 0 0 1 512 470 Z" fill="url(#dome)" transform="translate(-62,0)"/> |
| | | <path d="M388 470 A62 62 0 0 1 512 470 Z" fill="url(#dome)"/> |
| | | <path d="M388 470 A62 62 0 0 1 512 470" fill="none" stroke="#D4AF37" stroke-width="1.6" opacity="0.6"/> |
| | | <rect x="444" y="382" width="12" height="28" fill="#17395E"/> |
| | | <circle cx="450" cy="376" r="8" fill="#D4AF37" opacity="0.9"/> |
| | | <!-- å¡å°å
ç¹ --> |
| | | <circle cx="450" cy="376" r="18" fill="#EBD07E" opacity="0.16"/> |
| | | |
| | | <!-- ===== å
æ ¹ç«æ± ===== --> |
| | | <g fill="#1B4168"> |
| | | <rect x="302" y="578" width="26" height="138"/> |
| | | <rect x="356" y="578" width="26" height="138"/> |
| | | <rect x="410" y="578" width="26" height="138"/> |
| | | <rect x="464" y="578" width="26" height="138"/> |
| | | <rect x="518" y="578" width="26" height="138"/> |
| | | <rect x="572" y="578" width="26" height="138"/> |
| | | </g> |
| | | <!-- æ±å¤´ä¸æ±ç¡ --> |
| | | <g fill="#24507C"> |
| | | <rect x="298" y="572" width="34" height="10"/> |
| | | <rect x="352" y="572" width="34" height="10"/> |
| | | <rect x="406" y="572" width="34" height="10"/> |
| | | <rect x="460" y="572" width="34" height="10"/> |
| | | <rect x="514" y="572" width="34" height="10"/> |
| | | <rect x="568" y="572" width="34" height="10"/> |
| | | <rect x="300" y="708" width="30" height="10"/> |
| | | <rect x="354" y="708" width="30" height="10"/> |
| | | <rect x="408" y="708" width="30" height="10"/> |
| | | <rect x="462" y="708" width="30" height="10"/> |
| | | <rect x="516" y="708" width="30" height="10"/> |
| | | <rect x="570" y="708" width="30" height="10"/> |
| | | </g> |
| | | |
| | | <!-- ===== æ±é´çªå
===== --> |
| | | <g fill="url(#winLight)"> |
| | | <rect x="332" y="612" width="20" height="34" opacity="0.82"/> |
| | | <rect x="386" y="612" width="20" height="34" opacity="0.62"/> |
| | | <rect x="440" y="612" width="20" height="34" opacity="0.9"/> |
| | | <rect x="494" y="612" width="20" height="34" opacity="0.58"/> |
| | | <rect x="548" y="612" width="20" height="34" opacity="0.78"/> |
| | | </g> |
| | | <g fill="url(#winLight)"> |
| | | <rect x="332" y="662" width="20" height="30" opacity="0.55"/> |
| | | <rect x="386" y="662" width="20" height="30" opacity="0.72"/> |
| | | <rect x="440" y="662" width="20" height="30" opacity="0.6"/> |
| | | <rect x="494" y="662" width="20" height="30" opacity="0.8"/> |
| | | <rect x="548" y="662" width="20" height="30" opacity="0.5"/> |
| | | </g> |
| | | |
| | | <!-- ä¸å¤®å¤§é¨ --> |
| | | <path d="M432 716 L432 676 A18 18 0 0 1 468 676 L468 716 Z" fill="#0E2743"/> |
| | | <path d="M432 716 L432 676 A18 18 0 0 1 468 676 L468 716" fill="none" stroke="#D4AF37" stroke-width="1.4" opacity="0.6"/> |
| | | <rect x="448" y="676" width="1.6" height="40" fill="#D4AF37" opacity="0.45"/> |
| | | |
| | | <!-- é¨åç两çå£ç¯ --> |
| | | <circle cx="414" cy="700" r="5" fill="#EBD07E" opacity="0.9"/> |
| | | <circle cx="414" cy="700" r="14" fill="#EBD07E" opacity="0.16"/> |
| | | <circle cx="486" cy="700" r="5" fill="#EBD07E" opacity="0.9"/> |
| | | <circle cx="486" cy="700" r="14" fill="#EBD07E" opacity="0.16"/> |
| | | |
| | | <!-- ===== ä¸¤ä¾§æ æ¨ ===== --> |
| | | <g fill="#0A1B2F"> |
| | | <!-- å·¦æ --> |
| | | <rect x="146" y="700" width="10" height="60"/> |
| | | <ellipse cx="151" cy="666" rx="52" ry="42"/> |
| | | <ellipse cx="120" cy="690" rx="34" ry="28"/> |
| | | <ellipse cx="182" cy="688" rx="30" ry="26"/> |
| | | <!-- 峿 --> |
| | | <rect x="742" y="704" width="10" height="56"/> |
| | | <ellipse cx="747" cy="672" rx="48" ry="40"/> |
| | | <ellipse cx="718" cy="694" rx="32" ry="26"/> |
| | | <ellipse cx="776" cy="692" rx="28" ry="24"/> |
| | | </g> |
| | | <!-- æ å åå
è¾¹ --> |
| | | <g fill="none" stroke="#D4AF37" stroke-width="1.1" opacity="0.18"> |
| | | <ellipse cx="151" cy="666" rx="52" ry="42"/> |
| | | <ellipse cx="747" cy="672" rx="48" ry="40"/> |
| | | </g> |
| | | |
| | | <!-- ===== åæ¯æ°´é¢ ===== --> |
| | | <rect x="0" y="755" width="900" height="145" fill="url(#pool)"/> |
| | | <rect x="0" y="755" width="900" height="1.6" fill="#D4AF37" opacity="0.22"/> |
| | | |
| | | <!-- 建ç卿°´é¢çåå½± --> |
| | | <g opacity="0.5"> |
| | | <rect x="300" y="758" width="300" height="66" fill="url(#facade)" opacity="0.35"/> |
| | | <rect x="330" y="758" width="18" height="52" fill="#D4AF37" opacity="0.16"/> |
| | | <rect x="384" y="758" width="18" height="52" fill="#D4AF37" opacity="0.12"/> |
| | | <rect x="438" y="758" width="18" height="52" fill="#D4AF37" opacity="0.2"/> |
| | | <rect x="492" y="758" width="18" height="52" fill="#D4AF37" opacity="0.12"/> |
| | | <rect x="546" y="758" width="18" height="52" fill="#D4AF37" opacity="0.16"/> |
| | | </g> |
| | | <rect x="0" y="755" width="900" height="120" fill="url(#reflect)"/> |
| | | |
| | | <!-- æ°´é¢æ³¢çº¹ --> |
| | | <g stroke="#EBD07E" stroke-width="1.2" opacity="0.16" stroke-linecap="round"> |
| | | <line x1="286" y1="792" x2="356" y2="792"/> |
| | | <line x1="404" y1="806" x2="490" y2="806"/> |
| | | <line x1="520" y1="786" x2="596" y2="786"/> |
| | | <line x1="330" y1="826" x2="420" y2="826"/> |
| | | <line x1="470" y1="842" x2="556" y2="842"/> |
| | | <line x1="180" y1="812" x2="238" y2="812"/> |
| | | <line x1="660" y1="820" x2="726" y2="820"/> |
| | | </g> |
| | | |
| | | <!-- ===== åºé¨åæ ===== --> |
| | | <rect width="900" height="900" fill="url(#vignette)"/> |
| | | </svg> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace ArchiveBorrowApi { |
| | | /** åé
å */ |
| | | export interface Borrow { |
| | | id?: number; // åé
åç¼å· |
| | | no?: string; // åé
åå·ï¼å端çæï¼JY+yyyyMMdd+6使µæ°´ï¼åªè¯»ï¼ |
| | | documentId: number; // æ¡£æ¡ ID |
| | | documentNo?: string; // æ¡£æ¡ç¼å·ï¼å¿«ç
§ï¼ |
| | | documentTitle?: string; // æ¡£æ¡æ é¢ï¼å¿«ç
§ï¼ |
| | | borrowerUserId?: number; // åé
äººç¨æ· ID |
| | | borrowerName: string; // åé
人å§å |
| | | borrowerDept?: string; // åé
人é¨é¨ |
| | | borrowerPhone?: string; // åé
人èç³»çµè¯ |
| | | borrowDate: string; // åé
æ¥æ |
| | | borrowPurpose?: string; // åé
ç®ç |
| | | returnDeadline: string; // å½è¿æé |
| | | status?: number; // ç¶æï¼åå
¸ archive_borrow_status |
| | | processInstanceId?: string; // BPM æµç¨å®ä¾ ID |
| | | approveTime?: string; // 审æ¹éè¿ï¼å®é
ååºï¼æ¶é´ |
| | | returnTime?: string; // å®é
å½è¿æ¶é´ |
| | | returnConfirmUserId?: number; // å½è¿ç¡®è®¤äººç¨æ· ID |
| | | returnConfirmUserName?: string; // å½è¿ç¡®è®¤äººå§å |
| | | returnRemark?: string; // å½è¿å¤æ³¨ |
| | | rejectReason?: string; // 驳ååå |
| | | rejectTime?: string; // é©³åæ¶é´ |
| | | remark?: string; // 夿³¨ |
| | | overdue?: boolean; // æ¯å¦é¾æï¼åç«¯å®æ¶è®¡ç®ï¼ |
| | | overdueDays?: number; // 龿天æ°ï¼åç«¯å®æ¶è®¡ç®ï¼ |
| | | createTime?: string; // å建æ¶é´ |
| | | } |
| | | |
| | | /** å®¡æ¹æµç¨å®ä¹ */ |
| | | export interface ProcessDefinition { |
| | | id: string; |
| | | key: string; |
| | | name: string; |
| | | } |
| | | |
| | | /** å½è¿ç»è®°è¯·æ± */ |
| | | export interface ReturnReq { |
| | | id: number; // åé
åç¼å· |
| | | returnRemark?: string; // å½è¿å¤æ³¨ |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢åé
åå页 */ |
| | | export function getBorrowPage(params: PageParam) { |
| | | return requestClient.get<PageResult<ArchiveBorrowApi.Borrow>>( |
| | | '/archive/borrow/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢é¾ææªè¿åé
åå页 */ |
| | | export function getOverdueBorrowPage(params: PageParam) { |
| | | return requestClient.get<PageResult<ArchiveBorrowApi.Borrow>>( |
| | | '/archive/borrow/overdue-page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢åé
å详æ
*/ |
| | | export function getBorrow(id: number) { |
| | | return requestClient.get<ArchiveBorrowApi.Borrow>( |
| | | `/archive/borrow/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢åé
å */ |
| | | export function createBorrow(data: ArchiveBorrowApi.Borrow) { |
| | | return requestClient.post('/archive/borrow/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹åé
å */ |
| | | export function updateBorrow(data: ArchiveBorrowApi.Borrow) { |
| | | return requestClient.put('/archive/borrow/update', data); |
| | | } |
| | | |
| | | /** å é¤åé
å */ |
| | | export function deleteBorrow(id: number) { |
| | | return requestClient.delete('/archive/borrow/delete', { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** æäº¤åé
åå®¡æ¹ */ |
| | | export function submitBorrow(id: number, processDefinitionKey?: string) { |
| | | return requestClient.post('/archive/borrow/submit', null, { |
| | | params: { id, processDefinitionKey }, |
| | | }); |
| | | } |
| | | |
| | | /** è·åæ¡£æ¡åé
å®¡æ¹æµç¨å表 */ |
| | | export function getBorrowApproveProcessList() { |
| | | return requestClient.get<ArchiveBorrowApi.ProcessDefinition[]>( |
| | | '/archive/borrow/approve-process-list', |
| | | ); |
| | | } |
| | | |
| | | /** å½è¿ç»è®° */ |
| | | export function returnBorrow(data: ArchiveBorrowApi.ReturnReq) { |
| | | return requestClient.post('/archive/borrow/return', data); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace ArchiveCategoryApi { |
| | | /** æ¡£æ¡åç±»ï¼ä¹¦æ¶/åç±»æ ï¼ */ |
| | | export interface Category { |
| | | id?: number; // åç±»ç¼å· |
| | | parentId?: number; // ç¶åç±» IDï¼0=顶级 |
| | | name?: string; // åç±»åç§° |
| | | code?: string; // åç±»ç¼ç ï¼å¯ä¸ï¼member/research/meeting/finance/document/partyï¼ |
| | | sort?: number; // æ¾ç¤ºæåº |
| | | description?: string; // å类说æ |
| | | defaultSecretLevel?: number; // 该åç±»ä¸æ¡£æ¡é»è®¤å¯çº§ï¼åå
¸ archive_secret_level |
| | | status?: number; // ç¶æï¼åå
¸ archive_category_status |
| | | remark?: string; // 夿³¨ |
| | | createTime?: string; // å建æ¶é´ |
| | | children?: Category[]; // ååç±»ï¼æ å½¢è¿åï¼ |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¡£æ¡åç±»å表ï¼å¹³éºï¼ */ |
| | | export function getCategoryList(params?: ArchiveCategoryApi.Category) { |
| | | return requestClient.get<ArchiveCategoryApi.Category[]>( |
| | | '/archive/category/list', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¡£æ¡åç±»æ */ |
| | | export function getCategoryTree() { |
| | | return requestClient.get<ArchiveCategoryApi.Category[]>( |
| | | '/archive/category/tree', |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¡£æ¡å类详æ
*/ |
| | | export function getCategory(id: number) { |
| | | return requestClient.get<ArchiveCategoryApi.Category>( |
| | | `/archive/category/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢æ¡£æ¡åç±» */ |
| | | export function createCategory(data: ArchiveCategoryApi.Category) { |
| | | return requestClient.post('/archive/category/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹æ¡£æ¡åç±» */ |
| | | export function updateCategory(data: ArchiveCategoryApi.Category) { |
| | | return requestClient.put('/archive/category/update', data); |
| | | } |
| | | |
| | | /** å 餿¡£æ¡åç±» */ |
| | | export function deleteCategory(id: number) { |
| | | return requestClient.delete('/archive/category/delete', { |
| | | params: { id }, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import type { SystemStorageApi } from '#/api/system/storage'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace ArchiveDocumentApi { |
| | | /** æ¡£æ¡ï¼çµåéä»¶ + å®ä½åæ¾ä½ç½®ï¼ */ |
| | | export interface Document { |
| | | id?: number; // æ¡£æ¡ç¼å· |
| | | no?: string; // æ¡£æ¡ç¼å·ï¼å端çæï¼DA+yyyyMMdd+6使µæ°´ï¼åªè¯»ï¼ |
| | | title: string; // æ¡£æ¡æ é¢ |
| | | categoryId: number; // æ¡£æ¡åç±» ID |
| | | categoryName?: string; // åç±»åç§°ï¼å端åä½ï¼ |
| | | secretLevel?: number; // å¯çº§ï¼åå
¸ archive_secret_level |
| | | carrierType?: number; // è½½ä½ï¼åå
¸ archive_carrier_type |
| | | shelfNo?: string; // å®ä½-书æ¶å· |
| | | layerNo?: string; // å®ä½-å±å· |
| | | columnNo?: string; // å®ä½-åå· |
| | | boxNo?: string; // å®ä½-åå·/æ¡£æ¡çå· |
| | | storePlace?: string; // å®ä½åæ¾ä½ç½®æè¿° |
| | | sourceType?: number; // æ¥æºï¼åå
¸ archive_source_typeï¼å端åï¼å端åªè¯»ï¼ |
| | | sourceNo?: string; // æ¥æºåå·ï¼ç«é¡¹ç¼å·/ä¼åç¼å·/æ¥éåå·ï¼ |
| | | sourceId?: number; // æ¥æºåæ® ID |
| | | sourceTitle?: string; // æ¥æºåæ®æ é¢ |
| | | archiveDate?: string; // 彿¡£æ¥æ |
| | | archivistUserId?: number; // 彿¡£äººç¨æ· ID |
| | | archivistName?: string; // 彿¡£äººå§å |
| | | status?: number; // æ¡£æ¡ç¶æï¼åå
¸ archive_document_status |
| | | summary?: string; // æè¦ |
| | | tags?: string; // å
³é®è¯ï¼è±æéå·åé |
| | | remark?: string; // 夿³¨ |
| | | createTime?: string; // å建æ¶é´ |
| | | attachmentList?: SystemStorageApi.StorageBlob[]; // éä»¶å表 |
| | | hasAttachment?: boolean; // æ¯å¦æéä»¶ |
| | | blobIds?: number[]; // æäº¤æ¶çéä»¶ ID å表ï¼å
¨éï¼ |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¡£æ¡å页 */ |
| | | export function getDocumentPage(params: PageParam) { |
| | | return requestClient.get<PageResult<ArchiveDocumentApi.Document>>( |
| | | '/archive/document/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¡£æ¡è¯¦æ
*/ |
| | | export function getDocument(id: number) { |
| | | return requestClient.get<ArchiveDocumentApi.Document>( |
| | | `/archive/document/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢æ¡£æ¡ */ |
| | | export function createDocument(data: ArchiveDocumentApi.Document) { |
| | | return requestClient.post('/archive/document/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹æ¡£æ¡ */ |
| | | export function updateDocument(data: ArchiveDocumentApi.Document) { |
| | | return requestClient.put('/archive/document/update', data); |
| | | } |
| | | |
| | | /** å 餿¡£æ¡ */ |
| | | export function deleteDocument(id: number) { |
| | | return requestClient.delete('/archive/document/delete', { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** å¯¼åºæ¡£æ¡ Excel */ |
| | | export function exportDocument(params: ArchiveDocumentApi.Document) { |
| | | return requestClient.download('/archive/document/export-excel', { params }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace ArchiveStatApi { |
| | | /** å类维度ç»è®¡é¡¹ */ |
| | | export interface StatCategory { |
| | | categoryId?: number; // åç±» ID |
| | | name?: string; // åç±»åç§° |
| | | total?: number; // æ¡£æ¡æ°é |
| | | } |
| | | |
| | | /** åå
¸å¼ç»´åº¦ç»è®¡é¡¹ */ |
| | | export interface StatValue { |
| | | value?: number; // åå
¸å¼ |
| | | name?: string; // åå
¸æ ç¾ï¼å端转ä¹ï¼ |
| | | total?: number; // æ¡£æ¡æ°é |
| | | } |
| | | |
| | | /** æ¡£æ¡ç»è®¡æ¦è§ */ |
| | | export interface StatOverview { |
| | | totalCount?: number; // æ¡£æ¡æ»æ° |
| | | inStockCount?: number; // å¨åºæ¡£æ¡æ° |
| | | borrowedCount?: number; // ååºä¸æ¡£æ¡æ° |
| | | borrowingCount?: number; // ååºä¸åé
åæ° |
| | | overdueCount?: number; // 龿æªè¿åé
åæ° |
| | | byCategory?: StatCategory[]; // æåç±»åå¸ |
| | | bySecretLevel?: StatValue[]; // æå¯çº§åå¸ |
| | | byCarrierType?: StatValue[]; // æè½½ä½åå¸ |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¡£æ¡ç»è®¡æ¦è§ */ |
| | | export function getArchiveStatOverview() { |
| | | return requestClient.get<ArchiveStatApi.StatOverview>( |
| | | '/archive/stat/overview', |
| | | ); |
| | | } |
| | |
| | | applyDate: string; // ç³è¯·æ¥æ yyyy-MM-dd |
| | | processInstanceId?: string; // BPM æµç¨å®ä¾ ID |
| | | archiveTime?: string; // 彿¡£æ¶é´ |
| | | archiveDocumentId?: number; // 彿¡£çæçæ¡£æ¡ IDï¼é空å³å·²çææ¡£æ¡ï¼ |
| | | archived?: boolean; // æ¯å¦å·²çææ¡£æ¡ |
| | | remark?: string; // 夿³¨ |
| | | creator?: string; // å建人 ID |
| | | creatorName?: string; // å建人å§å |
| | |
| | | }); |
| | | } |
| | | |
| | | /** 彿¡£æ¥éåï¼ä»
å·²éè¿å¯å½æ¡£ï¼ */ |
| | | /** 彿¡£æ¥éåï¼ä»
å·²éè¿å¯å½æ¡£ï¼ï¼è¿åçæçæ¡£æ¡ç¼å· */ |
| | | export function archiveReimbursement(id: number) { |
| | | return requestClient.post('/finance/reimbursement/archive', null, { |
| | | return requestClient.post<string>('/finance/reimbursement/archive', null, { |
| | | params: { id }, |
| | | }); |
| | | } |
| | |
| | | researchNo?: string; // ç«é¡¹ç¼å·ï¼å®¡æ¹éè¿åçæ |
| | | approveTime?: string; // ç«é¡¹æ¶é´ |
| | | noticeId?: number; // å·²åå¸çç«é¡¹éç¥å
Œ ID |
| | | archiveDocumentId?: number; // 彿¡£çæçæ¡£æ¡ IDï¼é空å³å·²å½æ¡£ï¼ |
| | | archived?: boolean; // æ¯å¦å·²å½æ¡£ |
| | | remark?: string; // 夿³¨ |
| | | creator?: string; // å建人 ID |
| | | creatorName?: string; // å建人å§å |
| | |
| | | }); |
| | | } |
| | | |
| | | /** 彿¡£å°æ¡£æ¡ç®¡çï¼ä»
å·²ç«é¡¹å¯å½æ¡£ï¼ï¼è¿åçæçæ¡£æ¡ç¼å· */ |
| | | export function archiveApplication(id: number) { |
| | | return requestClient.post<string>('/research/application/archive', null, { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** æ¥è¯¢æç³æ¥ä¹¦çä¸å®¶è¯å®¡æç»å表 */ |
| | | export function getTopicReviewList(applicationId: number) { |
| | | return requestClient.get<ResearchApplicationApi.TopicReview[]>( |
| | |
| | | amount: number; // æ¨ä»éé¢ï¼å
ï¼ |
| | | payDate?: string; // æ¨ä»æ¥æ |
| | | payee?: string; // æ¶æ¬¾åä½/è´¦æ·å |
| | | status?: number; // æ¨ä»ç¶æï¼åå
¸ research_fund_status |
| | | status?: number; // æ¨ä»ç¶æï¼åå
¸ research_fund_statusï¼0-å¾
æ¨ä» 15-æ¥é审æ¹ä¸ 20-å·²æ¨ä» 30-已驳åï¼ |
| | | reimbursementId?: number; // å
³èè´¢å¡æ¥éå ID |
| | | reimbursementNo?: string; // å
³èè´¢å¡æ¥éåå· |
| | | rejectReason?: string; // è´¢å¡æ¥é驳ååå ï¼BPM ååï¼ |
| | | rejectTime?: string; // è´¢å¡æ¥éé©³åæ¶é´ |
| | | remark?: string; // 夿³¨ |
| | | creator?: string; // å建人 ID |
| | | creatorName?: string; // å建人å§å |
| | |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢ç»è´¹æ¨ä»è®°å½ */ |
| | | /** ç»è®°ç»è´¹æ¨ä»è®°å½ï¼ç»è®°åèªå¨çæå¹¶æäº¤è´¢å¡æ¥é审æ¹ï¼ */ |
| | | export function createFundRecord(data: ResearchFundRecordApi.FundRecord) { |
| | | return requestClient.post('/research/fund-record/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹ç»è´¹æ¨ä»è®°å½ï¼ä»
å¾
æ¨ä»å¯æ¹ï¼ */ |
| | | /** ä¿®æ¹å¹¶éæ°åèµ·æ¥éï¼ä»
ãå¾
æ¨ä»ãã已驳åã坿¹ï¼ */ |
| | | export function updateFundRecord(data: ResearchFundRecordApi.FundRecord) { |
| | | return requestClient.put('/research/fund-record/update', data); |
| | | } |
| | |
| | | joinDate?: string; // å
¥ä¼æ¥æ |
| | | expireDate?: string; // æææè³ |
| | | status?: number; // ç¶æï¼åå
¸ ums_member_status |
| | | archiveDocumentId?: number; // 彿¡£çæçæ¡£æ¡ IDï¼é空å³å·²å½æ¡£ï¼ |
| | | archived?: boolean; // æ¯å¦å·²å½æ¡£ |
| | | remark?: string; // 夿³¨ |
| | | createTime?: string; // å建æ¶é´ |
| | | attachmentList?: SystemStorageApi.StorageBlob[]; // éä»¶å表 |
| | |
| | | return requestClient.delete('/ums/member/delete', { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** 彿¡£å°æ¡£æ¡ç®¡çï¼ä»
æ£å¸¸ç¶æä¼åå¯å½æ¡£ï¼ï¼è¿åçæçæ¡£æ¡ç¼å· */ |
| | | export function archiveMember(id: number) { |
| | | return requestClient.post<string>('/ums/member/archive', null, { |
| | | params: { id }, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace WxCpActivityApi { |
| | | /** æ´»å¨ç¶æï¼0è稿 1å·²åå¸ 2å·²ç»æ 3已忶 */ |
| | | export const ACTIVITY_STATUS = { |
| | | DRAFT: 0, |
| | | PUBLISHED: 1, |
| | | FINISHED: 2, |
| | | CANCELED: 3, |
| | | } as const; |
| | | |
| | | /** æ¨éèå´ï¼0å
¨å 1æå®é¨é¨ 2æå®æå */ |
| | | export const PUSH_SCOPE = { |
| | | ALL: 0, |
| | | PARTY: 1, |
| | | USER: 2, |
| | | } as const; |
| | | |
| | | /** æ´»å¨ */ |
| | | export interface Activity { |
| | | id?: number; |
| | | title?: string; |
| | | content?: string; |
| | | coverUrl?: string; |
| | | location?: string; |
| | | startTime?: string; |
| | | endTime?: string; |
| | | signupStartTime?: string; |
| | | signupEndTime?: string; |
| | | /** æ¥åä¸éï¼0 表示ä¸é */ |
| | | signupLimit?: number; |
| | | signupCount?: number; |
| | | status?: number; |
| | | statusName?: string; |
| | | pushScope?: number; |
| | | pushScopeName?: string; |
| | | pushTarget?: string; |
| | | pushStatus?: number; |
| | | pushTime?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | /** æ´»å¨å页æ¥è¯¢åæ° */ |
| | | export interface ActivityPageParams extends PageParam { |
| | | title?: string; |
| | | status?: number; |
| | | pushStatus?: number; |
| | | startTime?: [string, string]; |
| | | } |
| | | |
| | | /** æ¥åè®°å½ */ |
| | | export interface Signup { |
| | | id?: number; |
| | | activityId?: number; |
| | | activityTitle?: string; |
| | | wxUserId?: string; |
| | | name?: string; |
| | | mobile?: string; |
| | | deptName?: string; |
| | | content?: string; |
| | | /** ç¶æï¼0å·²æ¥å 1已忶 2å·²ç¾å° */ |
| | | status?: number; |
| | | statusName?: string; |
| | | signupTime?: string; |
| | | checkInTime?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | /** æ¥åè®°å½å页æ¥è¯¢åæ° */ |
| | | export interface SignupPageParams extends PageParam { |
| | | activityId?: number; |
| | | name?: string; |
| | | mobile?: string; |
| | | status?: number; |
| | | } |
| | | |
| | | /** åç§°-æ°éç»è®¡é¡¹ */ |
| | | export interface NameCount { |
| | | name: string; |
| | | count: number; |
| | | } |
| | | |
| | | /** æ¥åç»è®¡ */ |
| | | export interface ActivityStats { |
| | | activityId: number; |
| | | activityTitle?: string; |
| | | signupLimit?: number; |
| | | totalSignup: number; |
| | | signedUpCount: number; |
| | | checkedInCount: number; |
| | | canceledCount: number; |
| | | /** ä¸éå颿¶ä¸º null */ |
| | | remaining?: null | number; |
| | | signupPercent?: null | number; |
| | | checkInPercent: number; |
| | | deptStats: NameCount[]; |
| | | dateStats: NameCount[]; |
| | | sourceStats: NameCount[]; |
| | | } |
| | | |
| | | /** æ¨éè¯·æ± */ |
| | | export interface PushParams { |
| | | id: number; |
| | | pushScope?: number; |
| | | pushTarget?: string; |
| | | } |
| | | |
| | | /** æ¨éç»æ */ |
| | | export interface PushResult { |
| | | success: boolean; |
| | | errCode?: null | number; |
| | | message?: string; |
| | | toUser?: string; |
| | | toParty?: string; |
| | | } |
| | | |
| | | /** æ¥åéä»¶ */ |
| | | export interface SignupAttachment { |
| | | id: number; |
| | | name?: string; |
| | | originalFilename?: string; |
| | | contentType?: string; |
| | | byteSize?: number; |
| | | url?: string; |
| | | previewURL?: string; |
| | | downloadURL?: string; |
| | | } |
| | | } |
| | | |
| | | /** è·å¾æ´»å¨å页 */ |
| | | export function getActivityPage(params: WxCpActivityApi.ActivityPageParams) { |
| | | return requestClient.get<PageResult<WxCpActivityApi.Activity>>( |
| | | '/wxcp/activity/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** è·å¾æ´»å¨è¯¦æ
*/ |
| | | export function getActivity(id: number) { |
| | | return requestClient.get<WxCpActivityApi.Activity>( |
| | | '/wxcp/activity/get', |
| | | { params: { id } }, |
| | | ); |
| | | } |
| | | |
| | | /** å建活å¨ï¼å为èç¨¿ï¼ */ |
| | | export function createActivity(data: WxCpActivityApi.Activity) { |
| | | return requestClient.post<number>('/wxcp/activity/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹æ´»å¨ */ |
| | | export function updateActivity(data: WxCpActivityApi.Activity) { |
| | | return requestClient.put<boolean>('/wxcp/activity/update', data); |
| | | } |
| | | |
| | | /** å 餿´»å¨ */ |
| | | export function deleteActivity(id: number) { |
| | | return requestClient.delete<boolean>('/wxcp/activity/delete', { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** å叿´»å¨ */ |
| | | export function publishActivity(id: number) { |
| | | return requestClient.put<boolean>('/wxcp/activity/publish', null, { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** ç»ææ´»å¨ */ |
| | | export function finishActivity(id: number) { |
| | | return requestClient.put<boolean>('/wxcp/activity/finish', null, { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** åæ¶æ´»å¨ */ |
| | | export function cancelActivity(id: number) { |
| | | return requestClient.put<boolean>('/wxcp/activity/cancel', null, { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** æ¨éæ´»å¨ç»ä¼ä¸å¾®ä¿¡æå */ |
| | | export function pushActivity(data: WxCpActivityApi.PushParams) { |
| | | return requestClient.post<WxCpActivityApi.PushResult>( |
| | | '/wxcp/activity/push', |
| | | data, |
| | | ); |
| | | } |
| | | |
| | | /** è·å¾æ´»å¨æ¥å页å°å */ |
| | | export function getActivitySignupUrl(id: number) { |
| | | return requestClient.get<string>('/wxcp/activity/signup-url', { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * è·å¾æ´»å¨æ¥åäºç»´ç ï¼PNG Blobï¼ |
| | | * |
| | | * 该æ¥å£è¿åå¾çäºè¿å¶ï¼è <img src> æ æ³æºå¸¦è¯·æ±å¤´ï¼ |
| | | * æä»¥è¿éç¨ raw 模å¼åå Blobï¼åç±è°ç¨æ¹è½¬æ objectURL æ¾ç¤º/ä¸è½½ã |
| | | */ |
| | | export function getActivityQrCodeBlob(id: number, size?: number) { |
| | | return requestClient.request('/wxcp/activity/qrcode', { |
| | | method: 'GET', |
| | | params: { id, size }, |
| | | responseType: 'blob', |
| | | responseReturn: 'raw', |
| | | }); |
| | | } |
| | | |
| | | /** è·å¾æ´»å¨æ¥åç»è®¡ */ |
| | | export function getActivityStats(id: number) { |
| | | return requestClient.get<WxCpActivityApi.ActivityStats>( |
| | | '/wxcp/activity/stats', |
| | | { params: { id } }, |
| | | ); |
| | | } |
| | | |
| | | /** è·å¾æ¥åè®°å½å页 */ |
| | | export function getSignupPage(params: WxCpActivityApi.SignupPageParams) { |
| | | return requestClient.get<PageResult<WxCpActivityApi.Signup>>( |
| | | '/wxcp/activity/signup/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** åæ¶æ¥å */ |
| | | export function cancelSignup(id: number) { |
| | | return requestClient.put<boolean>('/wxcp/activity/signup/cancel', null, { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** æ¥åç¾å° */ |
| | | export function checkInSignup(id: number) { |
| | | return requestClient.put<boolean>('/wxcp/activity/signup/check-in', null, { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** è·å¾ææ¡æ¥åè®°å½ä¸ä¼ çå¾ç/éä»¶ */ |
| | | export function getSignupAttachments(signupId: number) { |
| | | return requestClient.get<WxCpActivityApi.SignupAttachment[]>( |
| | | '/wxcp/activity/signup/attachments', |
| | | { params: { signupId } }, |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace WxCpMemberApi { |
| | | /** ä¼ä¸å¾®ä¿¡é¨é¨ */ |
| | | export interface Department { |
| | | id: number; |
| | | name?: string; |
| | | parentId?: number; |
| | | order?: number; |
| | | children?: Department[]; |
| | | } |
| | | |
| | | /** ä¼ä¸å¾®ä¿¡æå */ |
| | | export interface Member { |
| | | /** æåè´¦å·ï¼æ¨éæ¶ä½ä¸º pushTarget ä½¿ç¨ */ |
| | | userId: string; |
| | | name?: string; |
| | | mobile?: string; |
| | | email?: string; |
| | | avatar?: string; |
| | | position?: string; |
| | | /** 0æªå®ä¹ 1ç· 2女 */ |
| | | gender?: string; |
| | | /** 1å·²å¯ç¨ 2å·²ç¦ç¨ 4æªæ¿æ´» 5éåºä¼ä¸ */ |
| | | status?: number; |
| | | departmentIds?: number[]; |
| | | departmentNames?: string; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * è·å¾é¨é¨æ |
| | | * |
| | | * 注æï¼è¯¥æ¥å£å®æ¶è°ç¨ä¼ä¸å¾®ä¿¡ï¼æé¢çéå¶ï¼è°ç¨æ¹éèªè¡ç¼åï¼ |
| | | * ä¸è¦å¨è¾å
¥æ¡éæ¯æ²ä¸ä¸ªå就请æ±ä¸æ¬¡ã |
| | | */ |
| | | export function getWxCpDepartmentTree() { |
| | | return requestClient.get<WxCpMemberApi.Department[]>( |
| | | '/wxcp/member/department/tree', |
| | | ); |
| | | } |
| | | |
| | | /** è·å¾é¨é¨å¹³éºå表ï¼åä¸æéæ©æ´æ¹ä¾¿ï¼ */ |
| | | export function getWxCpDepartmentList() { |
| | | return requestClient.get<WxCpMemberApi.Department[]>( |
| | | '/wxcp/member/department/list', |
| | | ); |
| | | } |
| | | |
| | | /** è·å¾ä¼ä¸å¾®ä¿¡æåå表 */ |
| | | export function getWxCpMemberList(params?: { |
| | | departmentId?: number; |
| | | fetchChild?: boolean; |
| | | }) { |
| | | return requestClient.get<WxCpMemberApi.Member[]>('/wxcp/member/list', { |
| | | params, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace WxCpNoticeApi { |
| | | /** éç¥ç¶æï¼0è稿 1å·²åå¸ 2å·²æ¤å */ |
| | | export const NOTICE_STATUS = { |
| | | DRAFT: 0, |
| | | PUBLISHED: 1, |
| | | REVOKED: 2, |
| | | } as const; |
| | | |
| | | /** éç¥ */ |
| | | export interface Notice { |
| | | id?: number; |
| | | title?: string; |
| | | content?: string; |
| | | status?: number; |
| | | statusName?: string; |
| | | pushScope?: number; |
| | | pushScopeName?: string; |
| | | pushTarget?: string; |
| | | pushStatus?: number; |
| | | pushTime?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | /** éç¥å页æ¥è¯¢åæ° */ |
| | | export interface NoticePageParams extends PageParam { |
| | | title?: string; |
| | | status?: number; |
| | | pushStatus?: number; |
| | | } |
| | | } |
| | | |
| | | /** è·å¾éç¥å页 */ |
| | | export function getNoticePage(params: WxCpNoticeApi.NoticePageParams) { |
| | | return requestClient.get<PageResult<WxCpNoticeApi.Notice>>( |
| | | '/wxcp/notice/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** è·å¾éç¥è¯¦æ
*/ |
| | | export function getNotice(id: number) { |
| | | return requestClient.get<WxCpNoticeApi.Notice>('/wxcp/notice/get', { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** å建éç¥ï¼å为èç¨¿ï¼ */ |
| | | export function createNotice(data: WxCpNoticeApi.Notice) { |
| | | return requestClient.post<number>('/wxcp/notice/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹éç¥ */ |
| | | export function updateNotice(data: WxCpNoticeApi.Notice) { |
| | | return requestClient.put<boolean>('/wxcp/notice/update', data); |
| | | } |
| | | |
| | | /** å é¤éç¥ */ |
| | | export function deleteNotice(id: number) { |
| | | return requestClient.delete<boolean>('/wxcp/notice/delete', { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** åå¸éç¥ */ |
| | | export function publishNotice(id: number) { |
| | | return requestClient.put<boolean>('/wxcp/notice/publish', null, { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** æ¤åéç¥ */ |
| | | export function revokeNotice(id: number) { |
| | | return requestClient.put<boolean>('/wxcp/notice/revoke', null, { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** æ¨ééç¥ç»ä¼ä¸å¾®ä¿¡æå */ |
| | | export function pushNotice(data: { |
| | | id: number; |
| | | pushScope?: number; |
| | | pushTarget?: string; |
| | | }) { |
| | | return requestClient.post<{ |
| | | success: boolean; |
| | | errCode?: null | number; |
| | | message?: string; |
| | | }>('/wxcp/notice/push', data); |
| | | } |
| | |
| | | RESEARCH_FUND_STATUS: 'research_fund_status', // ç»è´¹æ¨ä»ç¶æ |
| | | } as const; |
| | | |
| | | /** ========== ARCHIVE - æ¡£æ¡ç®¡ç模å ========== */ |
| | | const ARCHIVE_DICT = { |
| | | ARCHIVE_CATEGORY_STATUS: 'archive_category_status', // æ¡£æ¡åç±»å¯ç¨/åç¨ |
| | | ARCHIVE_DOCUMENT_STATUS: 'archive_document_status', // æ¡£æ¡ç¶æï¼å¨åº/ååºä¸ï¼ |
| | | ARCHIVE_SECRET_LEVEL: 'archive_secret_level', // æ¡£æ¡å¯çº§ï¼å
¬å¼/å
é¨/ä¿å¯ï¼ |
| | | ARCHIVE_CARRIER_TYPE: 'archive_carrier_type', // æ¡£æ¡è½½ä½ï¼çµå/å®ä½/çµå+å®ä½ï¼ |
| | | ARCHIVE_SOURCE_TYPE: 'archive_source_type', // æ¡£æ¡æ¥æºï¼æå·¥/课é¢/ä¼å/è´¢å¡ï¼ |
| | | ARCHIVE_BORROW_STATUS: 'archive_borrow_status', // åé
åç¶æ |
| | | } as const; |
| | | |
| | | /** ========== AI - 人工æºè½æ¨¡å ========== */ |
| | | const AI_DICT = { |
| | | AI_GENERATE_MODE: 'ai_generate_mode', // AI çææ¨¡å¼ |
| | |
| | | const DICT_TYPE = { |
| | | ...AFTERSALES_DICT, |
| | | ...AI_DICT, |
| | | ...ARCHIVE_DICT, |
| | | ...BPM_DICT, |
| | | ...CRM_DICT, |
| | | ...ERP_DICT, |
| | |
| | | |
| | | import { Copyright } from '../basic/copyright'; |
| | | import AuthenticationFormView from './form.vue'; |
| | | import SloganIcon from './icons/slogan.vue'; |
| | | import Toolbar from './toolbar.vue'; |
| | | |
| | | interface Props { |
| | |
| | | </div> |
| | | </slot> |
| | | |
| | | <!-- ç³»ç»ä»ç» --> |
| | | <!-- ç³»ç»ä»ç»ï¼æ ¡å夿¯æç»éºæ»¡ + å·¦ä¸è§åçææ¡ --> |
| | | <div v-if="!authPanelCenter" class="relative hidden w-0 flex-1 lg:block"> |
| | | <div |
| | | class="absolute inset-0 size-full bg-background-deep dark:bg-[#070709]" |
| | | > |
| | | <div class="login-background absolute top-0 left-0 size-full"></div> |
| | | <!-- æç»æ ¡åæç»ï¼å¤é¨ä¼ å
¥ sloganImage æ¶ä¼å
ç¨å¤é¨å¾ --> |
| | | <img |
| | | v-if="sloganImage" |
| | | alt="" |
| | | class="campus-illustration" |
| | | :src="sloganImage" |
| | | /> |
| | | <img |
| | | v-else |
| | | alt="" |
| | | class="campus-illustration" |
| | | src="/static/imgs/jssm-campus.svg" |
| | | /> |
| | | <!-- åæå å±ï¼åºé¨å éï¼ä¿è¯æåå¯è¯» --> |
| | | <div class="campus-overlay"></div> |
| | | <div class="academic-bg-pattern"></div> |
| | | |
| | | <div |
| | | :key="authPanelLeft ? 'left' : authPanelRight ? 'right' : 'center'" |
| | | class="mr-20 flex-col-center h-full" |
| | | class="campus-content" |
| | | :class="{ |
| | | 'enter-x': authPanelLeft, |
| | | '-enter-x': authPanelRight, |
| | | }" |
| | | > |
| | | <template v-if="sloganImage"> |
| | | <img |
| | | :alt="appName" |
| | | :src="sloganImage" |
| | | class="h-64 w-2/5 animate-float" |
| | | /> |
| | | </template> |
| | | <SloganIcon v-else :alt="appName" class="h-64 w-2/5 animate-float" /> |
| | | <div class="text-1xl mt-6 font-sans text-foreground lg:text-2xl"> |
| | | {{ pageTitle }} |
| | | </div> |
| | | <div class="mt-2 dark:text-muted-foreground"> |
| | | {{ pageDescription }} |
| | | </div> |
| | | <div class="campus-rule"></div> |
| | | <div class="academic-brand-title">{{ pageTitle }}</div> |
| | | <div class="academic-brand-desc">{{ pageDescription }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | <!-- ä¸å¿è®¤è¯é¢æ¿ --> |
| | | <div v-if="authPanelCenter" class="relative flex-center w-full"> |
| | | <div class="login-background absolute top-0 left-0 size-full"></div> |
| | | <!-- 妿¯é£è£
é¥°ç½æ ¼èæ¯ --> |
| | | <div class="academic-bg-pattern"></div> |
| | | <AuthenticationFormView |
| | | class="w-full rounded-3xl pb-20 shadow-float shadow-primary/5 md:w-2/3 md:bg-background lg:w-1/2 xl:w-[36%]" |
| | | data-side="bottom" |
| | |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .login-background { |
| | | background: linear-gradient( |
| | | 154deg, |
| | | #07070915 30%, |
| | | hsl(var(--primary) / 30%) 48%, |
| | | #07070915 64% |
| | | ); |
| | | filter: blur(100px); |
| | | /* æ ¡åæç»éºæ»¡å·¦ä¾§åçåºï¼object-position ç¥åä¸ï¼ä¿ä½å»ºçä¸»ä½ */ |
| | | .campus-illustration { |
| | | position: absolute; |
| | | inset: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | object-fit: cover; |
| | | object-position: center 34%; |
| | | } |
| | | |
| | | /* å å±ï¼åºé¨åæç»æåï¼å·¦ä¾§ç¥æä»¥çªåºç½è²æ é¢ */ |
| | | .campus-overlay { |
| | | position: absolute; |
| | | inset: 0; |
| | | background: |
| | | linear-gradient( |
| | | 180deg, |
| | | rgba(8, 24, 44, 0.12) 0%, |
| | | rgba(8, 24, 44, 0.22) 42%, |
| | | rgba(8, 24, 44, 0.86) 100% |
| | | ), |
| | | linear-gradient( |
| | | 90deg, |
| | | rgba(8, 24, 44, 0.4) 0%, |
| | | rgba(8, 24, 44, 0) 58% |
| | | ); |
| | | } |
| | | |
| | | /* è£
é¥°ç½æ ¼ï¼åå°å ä¹ä¸å¯è§çéè²ç»çº¿ */ |
| | | .academic-bg-pattern { |
| | | position: absolute; |
| | | inset: 0; |
| | | background-image: |
| | | linear-gradient(rgba(212, 175, 55, 0.03) 1px, transparent 1px), |
| | | linear-gradient(90deg, rgba(212, 175, 55, 0.03) 1px, transparent 1px); |
| | | background-size: 72px 72px; |
| | | pointer-events: none; |
| | | } |
| | | |
| | | /* åçææ¡ï¼å®ä½å°å·¦ä¸è§ */ |
| | | .campus-content { |
| | | position: absolute; |
| | | bottom: 68px; |
| | | left: 56px; |
| | | z-index: 10; |
| | | max-width: 64%; |
| | | } |
| | | |
| | | /* æ é¢ä¸æ¹çéè²ç横线 */ |
| | | .campus-rule { |
| | | width: 46px; |
| | | height: 2px; |
| | | margin-bottom: 20px; |
| | | border-radius: 2px; |
| | | background: linear-gradient(90deg, #e3c567, #b8901f); |
| | | } |
| | | |
| | | /* åçåºæ é¢ï¼ç±³ç½ï¼é´å½±æ¶æä»¥ä¿æåºé */ |
| | | .academic-brand-title { |
| | | color: #f5f0e4 !important; |
| | | font-size: 26px; |
| | | font-weight: 600; |
| | | letter-spacing: 5px; |
| | | text-shadow: 0 1px 10px rgba(0, 0, 0, 0.4); |
| | | } |
| | | |
| | | .academic-brand-desc { |
| | | margin-top: 12px; |
| | | color: rgba(212, 175, 55, 0.85) !important; |
| | | font-size: 13.5px; |
| | | letter-spacing: 3px; |
| | | } |
| | | |
| | | .dark { |
| | | .login-background { |
| | | background: linear-gradient( |
| | | 154deg, |
| | | #07070915 30%, |
| | | hsl(var(--primary) / 20%) 48%, |
| | | #07070915 64% |
| | | ); |
| | | filter: blur(100px); |
| | | .academic-bg-pattern { |
| | | background-image: |
| | | linear-gradient(rgba(212, 175, 55, 0.045) 1px, transparent 1px), |
| | | linear-gradient(90deg, rgba(212, 175, 55, 0.045) 1px, transparent 1px); |
| | | background-size: 72px 72px; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | { |
| | | "welcomeBack": "Welcome Back", |
| | | "pageTitle": "Plug-and-play Admin system", |
| | | "pageDesc": "Efficient, versatile frontend template", |
| | | "pageTitle": "Unified Management Platform", |
| | | "pageDesc": "Collaboration · Data-driven · Standardized Process", |
| | | "loginSuccess": "Login Successful", |
| | | "loginSuccessDesc": "Welcome Back", |
| | | "loginSubtitle": "Enter your account details to manage your projects", |
| | | "loginSubtitle": "Enter your account details to sign in", |
| | | "selectAccount": "Quick Select Account", |
| | | "username": "Username", |
| | | "password": "Password", |
| | |
| | | "githubLogin": "Github Login", |
| | | "googleLogin": "Google Login", |
| | | "dingdingLogin": "Dingding Login", |
| | | "codeSubtitle": "Enter your phone number to start managing your project", |
| | | "codeSubtitle": "Enter your mobile number to sign in", |
| | | "code": "Security code", |
| | | "codeTip": "Security code required {0} characters", |
| | | "mobile": "Mobile", |
| | |
| | | "alignLeft": "Align Left", |
| | | "alignRight": "Align Right" |
| | | } |
| | | } |
| | | } |
| | |
| | | { |
| | | "welcomeBack": "欢è¿åæ¥", |
| | | "pageTitle": "å¼ç®±å³ç¨ç大åä¸åå°ç®¡çç³»ç»", |
| | | "pageDesc": "å·¥ç¨åã髿§è½ãè·¨ç»ä»¶åºçå端模ç", |
| | | "pageTitle": "ç»ä¸ç®¡çå¹³å°", |
| | | "pageDesc": "åååå
¬ · æ°æ®å³ç · æµç¨è§è", |
| | | "loginSuccess": "ç»å½æå", |
| | | "loginSuccessDesc": "欢è¿åæ¥", |
| | | "loginSubtitle": "请è¾å
¥æ¨çè´¦æ·ä¿¡æ¯ä»¥å¼å§ç®¡çæ¨ç项ç®", |
| | | "loginSubtitle": "请è¾å
¥è´¦å·ä¿¡æ¯ç»å½ç»ä¸ç®¡çå¹³å°", |
| | | "selectAccount": "å¿«ééæ©è´¦å·", |
| | | "username": "è´¦å·", |
| | | "password": "å¯ç ", |
| | |
| | | "githubLogin": "Githubç»å½", |
| | | "googleLogin": "Googleç»å½", |
| | | "dingdingLogin": "ééç»å½", |
| | | "codeSubtitle": "请è¾å
¥æ¨çææºå·ç 以å¼å§ç®¡çæ¨ç项ç®", |
| | | "codeSubtitle": "请è¾å
¥ææºå·ç ç»å½ç»ä¸ç®¡çå¹³å°", |
| | | "code": "éªè¯ç ", |
| | | "codeTip": "请è¾å
¥{0}ä½éªè¯ç ", |
| | | "mobile": "ææºå·ç ", |
| | |
| | | "alignLeft": "å±
å·¦", |
| | | "alignRight": "å±
å³" |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { AuthenticationLogin, Verification, z } from '@vben/common-ui'; |
| | | import { AuthenticationLogin, VbenButton, Verification, z } from '@vben/common-ui'; |
| | | import { isCaptchaEnable, isTenantEnable } from '@vben/hooks'; |
| | | import { SvgWeChatIcon } from '@vben/icons'; |
| | | import { $t } from '@vben/locales'; |
| | | import { useAccessStore } from '@vben/stores'; |
| | | |
| | |
| | | getCaptcha, |
| | | getTenantByWebsite, |
| | | getTenantSimpleList, |
| | | socialAuthRedirect, |
| | | } from '#/api/core/auth'; |
| | | import { useAuthStore } from '#/store'; |
| | | |
| | | defineOptions({ name: 'Login' }); |
| | | |
| | | // vite.config.ts 注å
¥çå
¬å¸ Logo è·¯å¾ |
| | | declare const __APP_LOGO__: string; |
| | | |
| | | const authStore = useAuthStore(); |
| | | const accessStore = useAccessStore(); |
| | |
| | | const verifyRef = ref(); |
| | | |
| | | const captchaType = 'blockPuzzle'; // éªè¯ç ç±»åï¼'blockPuzzle' | 'clickWord' |
| | | |
| | | /** åºç¨æ é¢ï¼ç»å½é¡µåçæåï¼ */ |
| | | const appTitle = import.meta.env.VITE_APP_TITLE || 'æ±èåè´¸'; |
| | | |
| | | /** åçå¾½æ ï¼å¤ç¨ vite 注å
¥çå
¬å¸ Logoï¼ç¼ºçåè½å°å
ç½®å¾æ */ |
| | | const brandLogo = __APP_LOGO__ || '/logos/JSSM.svg'; |
| | | |
| | | /** è·åç§æ·å表 */ |
| | | const tenantList = ref<AuthApi.TenantResult[]>([]); // ç§æ·å表 |
| | |
| | | fetchTenantList(); |
| | | }); |
| | | |
| | | // ========== ä¼ä¸å¾®ä¿¡ç»å½ ========== |
| | | |
| | | /** ä¼ä¸å¾®ä¿¡ç社交类åï¼å¯¹åºå端 SocialTypeEnum.WECHAT_ENTERPRISE */ |
| | | const WXCP_SOCIAL_TYPE = 30; |
| | | |
| | | const wxcpLoading = ref(false); |
| | | |
| | | /** |
| | | * ä¼ä¸å¾®ä¿¡æ«ç ç»å½ |
| | | * |
| | | * æµç¨ï¼åææå°å -> æ´é¡µè·³è½¬å°ä¼ä¸å¾®ä¿¡æ«ç 页 -> æ«ç åä¼ä¸å¾®ä¿¡å¸¦ code/state |
| | | * åè·³å° social-login é¡µé¢ -> 该页é¢è°ç¨ /system/auth/social-login æ¢å令ç |
| | | */ |
| | | async function handleWxcpLogin() { |
| | | try { |
| | | wxcpLoading.value = true; |
| | | // 注æï¼type éè¦å
encode 䏿¬¡ï¼é
å social-login 页é¢ç getUrlValue() 使ç¨ï¼ |
| | | // å¦åä¼ä¸å¾®ä¿¡åè·³å type åæ°ä¼è¢«åæï¼å¯¼è´æ æ³è¯å«ç¤¾äº¤ç±»å |
| | | const redirectUri = `${location.origin}/auth/social-login?${encodeURIComponent( |
| | | `type=${WXCP_SOCIAL_TYPE}`, |
| | | )}`; |
| | | window.location.href = await socialAuthRedirect( |
| | | WXCP_SOCIAL_TYPE, |
| | | redirectUri, |
| | | ); |
| | | } catch (error) { |
| | | console.error('ä¼ä¸å¾®ä¿¡ç»å½å¤±è´¥:', error); |
| | | wxcpLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | const formSchema = computed((): VbenFormSchema[] => { |
| | | return [ |
| | | // { |
| | | // component: 'VbenSelect', |
| | | // componentProps: { |
| | | // options: tenantList.value.map((item) => ({ |
| | | // label: item.name, |
| | | // value: item.id.toString(), |
| | | // })), |
| | | // placeholder: $t('authentication.tenantTip'), |
| | | // }, |
| | | // fieldName: 'tenantId', |
| | | // label: $t('authentication.tenant'), |
| | | // dependencies: { |
| | | // triggerFields: ['tenantId'], |
| | | // if: tenantEnable, |
| | | // trigger(values) { |
| | | // accessStore.setTenantId(values.tenantId ? Number(values.tenantId) : null); |
| | | // }, |
| | | // }, |
| | | // }, |
| | | { |
| | | component: 'VbenInput', |
| | | componentProps: { |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <div> |
| | | <div class="academic-login-wrapper"> |
| | | <!-- é¡¶é¨åçæ è¯ï¼å¾½æ + å¹³å°å + éè²åé线 --> |
| | | <div class="academic-login-header"> |
| | | <img :alt="appTitle" :src="brandLogo" class="academic-brand-logo" /> |
| | | <h2 class="academic-brand-name">{{ appTitle }}</h2> |
| | | <div class="academic-decoration-line"></div> |
| | | </div> |
| | | |
| | | <AuthenticationLogin |
| | | ref="loginRef" |
| | | :form-schema="formSchema" |
| | |
| | | :show-register="false" |
| | | :show-forget-password="false" |
| | | :show-doc-link="false" |
| | | class="academic-login-form" |
| | | @submit="handleLogin" |
| | | /> |
| | | > |
| | | <!-- èªå®ä¹æ é¢ï¼è¦ç欢è¿è¯ --> |
| | | <template #title> |
| | | <div class="academic-title-area"> |
| | | <h3 class="academic-title">è´¦å·ç»å½</h3> |
| | | <p class="academic-subtitle">{{ $t('authentication.loginSubtitle') }}</p> |
| | | </div> |
| | | </template> |
| | | |
| | | <!-- ç¬¬ä¸æ¹ç»å½ --> |
| | | <template #third-party-login> |
| | | <div class="academic-third-party"> |
| | | <div class="academic-divider"> |
| | | <span class="academic-divider-line"></span> |
| | | <span class="academic-divider-text">å
¶ä»ç»å½æ¹å¼</span> |
| | | <span class="academic-divider-line"></span> |
| | | </div> |
| | | <VbenButton |
| | | class="academic-wxcp-btn" |
| | | variant="outline" |
| | | :loading="wxcpLoading" |
| | | @click="handleWxcpLogin" |
| | | > |
| | | <SvgWeChatIcon class="mr-2 size-4" /> |
| | | ä¼ä¸å¾®ä¿¡ç»å½ |
| | | </VbenButton> |
| | | </div> |
| | | </template> |
| | | </AuthenticationLogin> |
| | | |
| | | <!-- éªè¯ç ï¼æ»åæ¼å¾ï¼ï¼captchaEnable æ¶å¯ç¨ --> |
| | | <Verification |
| | | ref="verifyRef" |
| | | v-if="captchaEnable" |
| | |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | /* ========== 妿¯ç§ç 飿 ¼ç»å½é¡µæ ·å¼ ========== */ |
| | | .academic-login-wrapper { |
| | | @apply relative flex flex-col w-full; |
| | | min-height: 460px; |
| | | } |
| | | |
| | | /* ---- é¡¶é¨åçæ è¯ ---- */ |
| | | .academic-login-header { |
| | | @apply flex flex-col items-center mb-8; |
| | | } |
| | | |
| | | .academic-brand-logo { |
| | | width: 52px; |
| | | height: 52px; |
| | | margin-bottom: 16px; |
| | | filter: drop-shadow(0 3px 10px rgba(15, 36, 64, 0.14)); |
| | | } |
| | | |
| | | .academic-brand-name { |
| | | font-size: 19px; |
| | | font-weight: 600; |
| | | letter-spacing: 5px; |
| | | /* æ«åå³ä¾§ä¼å¤åºä¸ä»½åè·ï¼ç¨çéç¼©è¿æµæ¶ï¼è§è§æçæ£å±
ä¸ */ |
| | | text-indent: 5px; |
| | | color: #0f2440; |
| | | margin: 0 0 14px; |
| | | } |
| | | |
| | | .academic-decoration-line { |
| | | position: relative; |
| | | width: 58px; |
| | | height: 2px; |
| | | background: linear-gradient( |
| | | 90deg, |
| | | transparent, |
| | | #d4af37 32%, |
| | | #d4af37 68%, |
| | | transparent |
| | | ); |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | /* é线ä¸å¤®çè±å½¢ç¹ç¼ */ |
| | | .academic-decoration-line::after { |
| | | content: ''; |
| | | position: absolute; |
| | | top: 50%; |
| | | left: 50%; |
| | | width: 6px; |
| | | height: 6px; |
| | | background: #d4af37; |
| | | border-radius: 1px; |
| | | transform: translate(-50%, -50%) rotate(45deg); |
| | | } |
| | | |
| | | /* ---- èªå®ä¹æ é¢ ---- */ |
| | | .academic-title-area { |
| | | @apply text-left mb-6; |
| | | } |
| | | |
| | | .academic-title { |
| | | font-size: 26px; |
| | | font-weight: 600; |
| | | color: #0f2440; |
| | | letter-spacing: 2px; |
| | | margin: 0 0 8px 0; |
| | | line-height: 1.35; |
| | | } |
| | | |
| | | .academic-subtitle { |
| | | font-size: 13.5px; |
| | | color: #8a94a6; |
| | | margin: 0; |
| | | letter-spacing: 0.3px; |
| | | } |
| | | |
| | | /* ---- è¦çé»è®¤æ é¢ ---- */ |
| | | :deep(.academic-login-form .mb-7) { |
| | | display: none; |
| | | } |
| | | |
| | | /* ---- è¾å
¥æ¡ï¼æç½åº + éè²èç¦ ---- */ |
| | | :deep(.academic-login-form .vben-input) { |
| | | border-radius: 8px; |
| | | border-color: #e6e2d8; |
| | | background: #fdfcfa; |
| | | transition: all 0.25s ease; |
| | | height: 48px; |
| | | } |
| | | |
| | | :deep(.academic-login-form .vben-input:hover) { |
| | | border-color: #c9b77a; |
| | | } |
| | | |
| | | :deep(.academic-login-form .vben-input:focus-within) { |
| | | border-color: #d4af37; |
| | | background: #fff; |
| | | box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.13); |
| | | } |
| | | |
| | | /* ---- ç»å½æé®ï¼è鿏å + éè²æè¾¹ä¸æå ---- */ |
| | | :deep(.academic-login-form .w-full > button), |
| | | :deep(.academic-login-form button[aria-label="login"]) { |
| | | height: 48px; |
| | | border-radius: 8px; |
| | | background: linear-gradient(135deg, #1e4571, #0f2440) !important; |
| | | border: 1px solid rgba(212, 175, 55, 0.45) !important; |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | letter-spacing: 6px; |
| | | text-indent: 6px; |
| | | box-shadow: 0 6px 18px rgba(15, 36, 64, 0.22); |
| | | transition: all 0.3s ease; |
| | | color: #f5e7c0 !important; |
| | | } |
| | | |
| | | /* æ¬ååªå æ·±æè¾¹ä¸æå½±ï¼ä¸åä½ç§»ï¼ä¿æå
å¶ */ |
| | | :deep(.academic-login-form .w-full > button:hover), |
| | | :deep(.academic-login-form button[aria-label="login"]:hover) { |
| | | box-shadow: |
| | | 0 8px 22px rgba(15, 36, 64, 0.26), |
| | | 0 0 0 1px rgba(212, 175, 55, 0.6); |
| | | } |
| | | |
| | | :deep(.academic-login-form .w-full > button:active), |
| | | :deep(.academic-login-form button[aria-label="login"]:active) { |
| | | box-shadow: 0 3px 12px rgba(15, 36, 64, 0.24); |
| | | } |
| | | |
| | | /* ---- è®°ä½è´¦å· / å¿è®°å¯ç ---- */ |
| | | :deep(.academic-login-form .vben-link) { |
| | | color: #b8901f; |
| | | font-weight: 500; |
| | | transition: color 0.2s; |
| | | } |
| | | |
| | | :deep(.academic-login-form .vben-link:hover) { |
| | | color: #8a6a12; |
| | | } |
| | | |
| | | /* ---- ç¬¬ä¸æ¹ç»å½ ---- */ |
| | | .academic-third-party { |
| | | @apply mt-5; |
| | | } |
| | | |
| | | .academic-divider { |
| | | @apply flex items-center gap-3 mb-4; |
| | | } |
| | | |
| | | .academic-divider-line { |
| | | flex: 1; |
| | | height: 1px; |
| | | background: linear-gradient(90deg, transparent, #e2e8f0, transparent); |
| | | } |
| | | |
| | | .academic-divider-text { |
| | | @apply text-xs; |
| | | color: #94a3b8; |
| | | white-space: nowrap; |
| | | letter-spacing: 0.5px; |
| | | } |
| | | |
| | | .academic-wxcp-btn { |
| | | @apply w-full rounded-lg; |
| | | height: 44px; |
| | | border-color: #e6e2d8; |
| | | color: #44546a; |
| | | font-weight: 500; |
| | | transition: all 0.3s; |
| | | } |
| | | |
| | | .academic-wxcp-btn:hover { |
| | | border-color: #d4af37; |
| | | color: #0f2440; |
| | | background: rgba(212, 175, 55, 0.06); |
| | | } |
| | | |
| | | /* ---- æè²éé
---- */ |
| | | :root.dark .academic-title { |
| | | color: #f0ead8; |
| | | } |
| | | |
| | | :root.dark .academic-brand-name { |
| | | color: #f0ead8; |
| | | } |
| | | |
| | | :root.dark .academic-subtitle { |
| | | color: #8a94a6; |
| | | } |
| | | |
| | | :root.dark .academic-decoration-line { |
| | | background: linear-gradient(90deg, #8bb4f0, #4a7ad4); |
| | | } |
| | | |
| | | :root.dark .academic-footer-text { |
| | | color: #64748b; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ArchiveBorrowApi } from '#/api/archive/borrow'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { getDocumentPage } from '#/api/archive/document'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'edit'; |
| | | |
| | | /** åé
åç¶æï¼å¯¹åºåå
¸ archive_borrow_statusï¼ */ |
| | | export const BorrowStatus = { |
| | | DRAFT: 0, // è稿 |
| | | APPROVING: 15, // 审æ¹ä¸ |
| | | BORROWED: 20, // ååºä¸ |
| | | RETURNED: 30, // å·²å½è¿ |
| | | REJECTED: 35, // 已驳å |
| | | } as const; |
| | | |
| | | /** åé
ç»è®°è¡¨åé
ç½® */ |
| | | export function useFormSchema( |
| | | formType: FormType, |
| | | documentId?: number, |
| | | ): VbenFormSchema[] { |
| | | const readonly = formType === 'detail'; |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'documentId', |
| | | label: 'åé
æ¡£æ¡', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | allowClear: true, |
| | | showSearch: true, |
| | | placeholder: 'è¯·éæ©å¨åºæ¡£æ¡', |
| | | api: async () => { |
| | | const { list } = await getDocumentPage({ |
| | | pageNo: 1, |
| | | pageSize: 100, |
| | | status: 10, |
| | | }); |
| | | return list.map((item) => ({ |
| | | id: item.id, |
| | | label: item.no ? `${item.no} ${item.title}` : item.title, |
| | | })); |
| | | }, |
| | | labelField: 'label', |
| | | valueField: 'id', |
| | | disabled: readonly || !!documentId, |
| | | }, |
| | | rules: 'selectRequired', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'borrowerUserId', |
| | | label: 'åé
人', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | allowClear: true, |
| | | showSearch: true, |
| | | placeholder: 'è¯·éæ©åé
人', |
| | | api: getSimpleUserList, |
| | | labelField: 'nickname', |
| | | valueField: 'id', |
| | | disabled: readonly, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'borrowerName', |
| | | label: 'åé
人å§å', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥åé
人å§å', |
| | | disabled: readonly, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'borrowerDept', |
| | | label: 'åé
人é¨é¨', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥åé
人é¨é¨', |
| | | disabled: readonly, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'borrowerPhone', |
| | | label: 'èç³»çµè¯', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥èç³»çµè¯', |
| | | disabled: readonly, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'borrowDate', |
| | | label: 'åé
æ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | allowClear: true, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | placeholder: 'è¯·éæ©åé
æ¥æ', |
| | | disabled: readonly, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'returnDeadline', |
| | | label: 'å½è¿æé', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | allowClear: true, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | placeholder: 'è¯·éæ©å½è¿æé', |
| | | disabled: readonly, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'borrowPurpose', |
| | | label: 'åé
ç®ç', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | rows: 2, |
| | | placeholder: '请è¾å
¥åé
ç®ç', |
| | | disabled: readonly, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | rows: 2, |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | disabled: readonly, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'no', |
| | | label: 'åé
åå·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥åé
åå·', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'documentNo', |
| | | label: 'æ¡£æ¡ç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥æ¡£æ¡ç¼å·', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'documentTitle', |
| | | label: 'æ¡£æ¡æ é¢', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥æ¡£æ¡æ é¢', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'borrowerName', |
| | | label: 'åé
人', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥åé
人å§å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.ARCHIVE_BORROW_STATUS, 'number'), |
| | | allowClear: true, |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'borrowDate', |
| | | label: 'åé
æ¥æ', |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | allowClear: true, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | placeholder: ['å¼å§æ¥æ', 'ç»ææ¥æ'], |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'returnDeadline', |
| | | label: 'å½è¿æé', |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | allowClear: true, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | placeholder: ['å¼å§æ¥æ', 'ç»ææ¥æ'], |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** è¡¨æ ¼åé
ç½® */ |
| | | export function useGridColumns(): VxeTableGridOptions<ArchiveBorrowApi.Borrow>['columns'] { |
| | | return [ |
| | | { |
| | | field: 'no', |
| | | title: 'åé
åå·', |
| | | minWidth: 170, |
| | | }, |
| | | { |
| | | field: 'documentNo', |
| | | title: 'æ¡£æ¡ç¼å·', |
| | | minWidth: 170, |
| | | }, |
| | | { |
| | | field: 'documentTitle', |
| | | title: 'æ¡£æ¡æ é¢', |
| | | minWidth: 200, |
| | | }, |
| | | { |
| | | field: 'borrowerName', |
| | | title: 'åé
人', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'borrowerDept', |
| | | title: 'é¨é¨', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'borrowDate', |
| | | title: 'åé
æ¥æ', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'returnDeadline', |
| | | title: 'å½è¿æé', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'overdue', |
| | | title: '龿', |
| | | minWidth: 110, |
| | | slots: { default: 'overdue' }, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | minWidth: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.ARCHIVE_BORROW_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'approveTime', |
| | | title: 'ååºæ¶é´', |
| | | minWidth: 170, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | field: 'returnTime', |
| | | title: 'å½è¿æ¶é´', |
| | | minWidth: 170, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 230, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { ArchiveBorrowApi } from '#/api/archive/borrow'; |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { h, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | |
| | | import { Spin, Tag } from 'ant-design-vue'; |
| | | |
| | | import { getBorrow } from '#/api/archive/borrow'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | import { useDescription } from '#/components/description'; |
| | | |
| | | defineOptions({ name: 'ArchiveBorrowApproveDetail' }); |
| | | |
| | | const props = defineProps<{ id: string }>(); |
| | | |
| | | const loading = ref(false); |
| | | const formData = ref<ArchiveBorrowApi.Borrow>(); |
| | | |
| | | /** ææ¬åæ®µç©ºå¼å ä½ */ |
| | | function textRender(val: unknown) { |
| | | return val === 0 || val ? String(val) : '-'; |
| | | } |
| | | |
| | | /** æ¥ææ¶é´å段空å¼å ä½ */ |
| | | function timeRender(val: unknown) { |
| | | return val ? (formatDateTime(val as string) as string) : '-'; |
| | | } |
| | | |
| | | /** 龿å±ç¤º */ |
| | | function overdueRender(_: unknown, record: ArchiveBorrowApi.Borrow) { |
| | | return record?.overdue |
| | | ? h(Tag, { color: 'error' }, () => `龿 ${record.overdueDays} 天`) |
| | | : h(Tag, { color: 'success' }, () => 'æªé¾æ'); |
| | | } |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: true, |
| | | column: 2, |
| | | schema: [ |
| | | { label: 'åé
åå·', field: 'no', render: textRender }, |
| | | { |
| | | label: 'ç¶æ', |
| | | field: 'status', |
| | | render: (val) => |
| | | h(DictTag, { type: DICT_TYPE.ARCHIVE_BORROW_STATUS, value: val }), |
| | | }, |
| | | { label: 'æ¡£æ¡ç¼å·', field: 'documentNo', render: textRender }, |
| | | { label: 'æ¡£æ¡æ é¢', field: 'documentTitle', render: textRender }, |
| | | { label: 'åé
人', field: 'borrowerName', render: textRender }, |
| | | { label: 'åé
人é¨é¨', field: 'borrowerDept', render: textRender }, |
| | | { label: 'èç³»çµè¯', field: 'borrowerPhone', render: textRender }, |
| | | { label: 'åé
æ¥æ', field: 'borrowDate', render: textRender }, |
| | | { label: 'å½è¿æé', field: 'returnDeadline', render: textRender }, |
| | | { |
| | | label: '龿æ
åµ', |
| | | field: 'overdue', |
| | | render: (val, record) => overdueRender(val, record as ArchiveBorrowApi.Borrow), |
| | | }, |
| | | { |
| | | label: 'ååºæ¶é´', |
| | | field: 'approveTime', |
| | | render: timeRender, |
| | | }, |
| | | { |
| | | label: 'å½è¿æ¶é´', |
| | | field: 'returnTime', |
| | | render: timeRender, |
| | | }, |
| | | { label: 'å½è¿ç¡®è®¤äºº', field: 'returnConfirmUserName', render: textRender }, |
| | | { |
| | | label: 'åé
ç®ç', |
| | | field: 'borrowPurpose', |
| | | span: 2, |
| | | render: textRender, |
| | | }, |
| | | { |
| | | label: 'å½è¿å¤æ³¨', |
| | | field: 'returnRemark', |
| | | span: 2, |
| | | render: textRender, |
| | | }, |
| | | { |
| | | label: '驳ååå ', |
| | | field: 'rejectReason', |
| | | span: 2, |
| | | render: textRender, |
| | | }, |
| | | { |
| | | label: '驳忶é´', |
| | | field: 'rejectTime', |
| | | render: timeRender, |
| | | }, |
| | | { label: 'å建æ¶é´', field: 'createTime', render: timeRender }, |
| | | { label: '夿³¨', field: 'remark', span: 2, render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | loading.value = true; |
| | | formData.value = await getBorrow(Number(props.id)); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <ContentWrap class="m-2"> |
| | | <Spin :spinning="loading" tip="å è½½ä¸..."> |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">æ¡£æ¡åé
åä¿¡æ¯</span> |
| | | </div> |
| | | <Descriptions :data="formData" /> |
| | | </div> |
| | | </Spin> |
| | | </ContentWrap> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ArchiveBorrowApi } from '#/api/archive/borrow'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteBorrow, |
| | | getBorrowApproveProcessList, |
| | | getBorrowPage, |
| | | getOverdueBorrowPage, |
| | | } from '#/api/archive/borrow'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { BorrowStatus, useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import ProcessSelectModal from './modules/process-select-modal.vue'; |
| | | import ReturnForm from './modules/return-form.vue'; |
| | | |
| | | /** åé
ç»è®° */ |
| | | defineOptions({ name: 'ArchiveBorrow' }); |
| | | |
| | | const router = useRouter(); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ProcessModal, processModalApi] = useVbenModal({ |
| | | connectedComponent: ProcessSelectModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ReturnModal, returnModalApi] = useVbenModal({ |
| | | connectedComponent: ReturnForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** æ¯å¦åªç龿æªè¿ */ |
| | | const overdueOnly = ref(false); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** 忢ãåªç龿ã */ |
| | | function handleToggleOverdue() { |
| | | overdueOnly.value = !overdueOnly.value; |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** ç»è®°åé
*/ |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | /** ç¼è¾åé
å */ |
| | | function handleEdit(row: ArchiveBorrowApi.Borrow) { |
| | | formModalApi.setData({ formType: 'edit', id: row.id }).open(); |
| | | } |
| | | |
| | | /** æ¥çåé
å详æ
*/ |
| | | function handleDetail(row: ArchiveBorrowApi.Borrow) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | /** å é¤åé
å */ |
| | | async function handleDelete(row: ArchiveBorrowApi.Borrow) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting', [row.no]), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await deleteBorrow(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', [row.no])); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æäº¤å®¡æ¹ - å
è·åæµç¨åè¡¨ï¼æµç¨ key ä¸åæ» */ |
| | | async function handleSubmit(row: ArchiveBorrowApi.Borrow) { |
| | | const hideLoading = message.loading({ |
| | | content: 'è·åå®¡æ¹æµç¨...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | const processList = await getBorrowApproveProcessList(); |
| | | hideLoading(); |
| | | if (!processList || processList.length === 0) { |
| | | message.warning( |
| | | 'ææ å¯ç¨å®¡æ¹æµç¨ï¼è¯·å
å° BPM æµç¨è®¾è®¡å¨å叿¡£æ¡åé
å®¡æ¹æµç¨', |
| | | ); |
| | | return; |
| | | } |
| | | processModalApi.setData({ id: row.id, processList }).open(); |
| | | } catch { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** å½è¿ç»è®° */ |
| | | function handleReturn(row: ArchiveBorrowApi.Borrow) { |
| | | returnModalApi.setData(row).open(); |
| | | } |
| | | |
| | | /** æ¥ç审æ¹è¿åº¦ */ |
| | | function handleProcessDetail(row: ArchiveBorrowApi.Borrow) { |
| | | if (!row.processInstanceId) { |
| | | message.warning('该åé
åå°æªæäº¤å®¡æ¹'); |
| | | return; |
| | | } |
| | | router.push({ |
| | | name: 'BpmProcessInstanceDetail', |
| | | query: { id: row.processInstanceId }, |
| | | }); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | const params = { |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }; |
| | | return overdueOnly.value |
| | | ? await getOverdueBorrowPage(params) |
| | | : await getBorrowPage(params); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<ArchiveBorrowApi.Borrow>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ProcessModal @success="handleRefresh" /> |
| | | <ReturnModal @success="handleRefresh" /> |
| | | <Grid table-title="åé
ç»è®°"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'ç»è®°åé
', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['archive:borrow:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: overdueOnly ? 'æ¥çå
¨é¨' : 'åªç龿', |
| | | type: overdueOnly ? 'primary' : 'default', |
| | | danger: overdueOnly, |
| | | icon: ACTION_ICON.SEARCH, |
| | | auth: ['archive:borrow:query'], |
| | | onClick: handleToggleOverdue, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #overdue="{ row }"> |
| | | <Tag v-if="row.overdue" color="error">龿 {{ row.overdueDays }} 天</Tag> |
| | | <span v-else>-</span> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.detail'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['archive:borrow:query'], |
| | | onClick: handleDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['archive:borrow:update'], |
| | | ifShow: () => |
| | | row.status === BorrowStatus.DRAFT || |
| | | row.status === BorrowStatus.REJECTED, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æäº¤å®¡æ¹', |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['archive:borrow:submit'], |
| | | ifShow: () => |
| | | row.status === BorrowStatus.DRAFT || |
| | | row.status === BorrowStatus.REJECTED, |
| | | onClick: handleSubmit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'å½è¿ç»è®°', |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['archive:borrow:return'], |
| | | ifShow: () => row.status === BorrowStatus.BORROWED, |
| | | onClick: handleReturn.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æ¥ç审æ¹', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['archive:borrow:query'], |
| | | ifShow: () => !!row.processInstanceId, |
| | | onClick: handleProcessDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['archive:borrow:delete'], |
| | | ifShow: () => row.status === BorrowStatus.DRAFT, |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.no]), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { ArchiveBorrowApi } from '#/api/archive/borrow'; |
| | | |
| | | import type { FormType } from '../data'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { $t } from '@vben/locales'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createBorrow, |
| | | getBorrow, |
| | | updateBorrow, |
| | | } from '#/api/archive/borrow'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | defineOptions({ name: 'ArchiveBorrowForm' }); |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const formType = ref<FormType>('create'); |
| | | /** 仿¡£æ¡å表ãç»è®°åé
ã带å
¥çæ¡£æ¡ ID */ |
| | | const fixedDocumentId = ref<number>(); |
| | | |
| | | const getTitle = computed(() => { |
| | | if (formType.value === 'create') { |
| | | return 'ç»è®°åé
'; |
| | | } |
| | | if (formType.value === 'edit') { |
| | | return $t('ui.actionTitle.edit', ['åé
å']); |
| | | } |
| | | return 'åé
å详æ
'; |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | labelWidth: 110, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'vertical', |
| | | schema: [], |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | /** åªæäº¤å端 SaveReqVO æ¥åçåæ®µ */ |
| | | async function buildSubmitData() { |
| | | const values = (await formApi.getValues()) as ArchiveBorrowApi.Borrow; |
| | | return { |
| | | id: values.id, |
| | | documentId: values.documentId, |
| | | borrowerUserId: values.borrowerUserId, |
| | | borrowerName: values.borrowerName, |
| | | borrowerDept: values.borrowerDept, |
| | | borrowerPhone: values.borrowerPhone, |
| | | borrowDate: values.borrowDate, |
| | | borrowPurpose: values.borrowPurpose, |
| | | returnDeadline: values.returnDeadline, |
| | | remark: values.remark, |
| | | } as ArchiveBorrowApi.Borrow; |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = await buildSubmitData(); |
| | | try { |
| | | await (formType.value === 'create' |
| | | ? createBorrow(data) |
| | | : updateBorrow(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | fixedDocumentId.value = undefined; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ |
| | | formType: FormType; |
| | | id?: number; |
| | | documentId?: number; |
| | | }>(); |
| | | formType.value = data.formType; |
| | | fixedDocumentId.value = data.documentId; |
| | | formApi.setState({ schema: useFormSchema(formType.value, data.documentId) }); |
| | | formApi.setDisabled(formType.value === 'detail'); |
| | | if (data?.id) { |
| | | modalApi.lock(); |
| | | try { |
| | | const detail = await getBorrow(data.id); |
| | | await formApi.setValues(detail, false); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | } else { |
| | | await formApi.resetForm(); |
| | | // æ°å»ºæ¶å¸¦åºæ¡£æ¡ä¸é»è®¤åé
æ¥æï¼ä»å¤©ï¼ |
| | | await formApi.setValues({ |
| | | documentId: data.documentId, |
| | | borrowDate: dayjs().format('YYYY-MM-DD'), |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | :title="getTitle" |
| | | class="w-2/3" |
| | | :close-on-click-modal="false" |
| | | :show-confirm-button="formType !== 'detail'" |
| | | > |
| | | <Form class="mx-3" /> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { ArchiveBorrowApi } from '#/api/archive/borrow'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message, Select } from 'ant-design-vue'; |
| | | |
| | | import { submitBorrow } from '#/api/archive/borrow'; |
| | | |
| | | defineOptions({ name: 'ArchiveBorrowProcessSelect' }); |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const borrowId = ref<number>(); |
| | | const processList = ref<ArchiveBorrowApi.ProcessDefinition[]>([]); |
| | | const selectedProcessKey = ref<string>(''); |
| | | |
| | | /** æµç¨ key åªè½æ¥èªåç«¯å®æ¶å表ï¼ç¦æ¢åç«¯åæ» */ |
| | | const processOptions = computed(() => |
| | | processList.value.map((item) => ({ label: item.name, value: item.key })), |
| | | ); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | if (!selectedProcessKey.value) { |
| | | message.warning('è¯·éæ©å®¡æ¹æµç¨'); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | await submitBorrow(borrowId.value!, selectedProcessKey.value); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success('æäº¤æå'); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | borrowId.value = undefined; |
| | | selectedProcessKey.value = ''; |
| | | processList.value = []; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ |
| | | id: number; |
| | | processList: ArchiveBorrowApi.ProcessDefinition[]; |
| | | }>(); |
| | | borrowId.value = data.id; |
| | | processList.value = data.processList || []; |
| | | // é»è®¤éæ©ç¬¬ä¸ä¸ªæµç¨ |
| | | if (processList.value.length > 0) { |
| | | selectedProcessKey.value = processList.value[0]!.key; |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="éæ©å®¡æ¹æµç¨" class="w-1/3"> |
| | | <div class="mb-4"> |
| | | <span class="mr-2">å®¡æ¹æµç¨ï¼</span> |
| | | <Select |
| | | v-model:value="selectedProcessKey" |
| | | :options="processOptions" |
| | | style="width: 240px" |
| | | placeholder="è¯·éæ©å®¡æ¹æµç¨" |
| | | /> |
| | | </div> |
| | | <p class="text-sm text-gray-500"> |
| | | æäº¤å便¬¡æµè½¬ï¼æ¡£æ¡ç®¡çåå®¡æ ¸ â å管é¢å¯¼å®¡æ¹ |
| | | </p> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { ArchiveBorrowApi } from '#/api/archive/borrow'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { returnBorrow } from '#/api/archive/borrow'; |
| | | |
| | | defineOptions({ name: 'ArchiveBorrowReturnForm' }); |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | /** å½åå½è¿çåé
å */ |
| | | const current = ref<ArchiveBorrowApi.Borrow>(); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | }, |
| | | wrapperClass: 'grid-cols-1', |
| | | layout: 'vertical', |
| | | schema: [ |
| | | { |
| | | fieldName: 'returnRemark', |
| | | label: 'å½è¿å¤æ³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | rows: 3, |
| | | placeholder: 'å¦å½è¿æ¶æ¡£æ¡å®å¥½ãæ¯å¦æç¼ºé¡µç', |
| | | }, |
| | | }, |
| | | ], |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const values = (await formApi.getValues()) as { returnRemark?: string }; |
| | | modalApi.lock(); |
| | | try { |
| | | await returnBorrow({ |
| | | id: current.value!.id!, |
| | | returnRemark: values.returnRemark, |
| | | }); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success('å½è¿ç»è®°æå'); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | current.value = undefined; |
| | | return; |
| | | } |
| | | current.value = modalApi.getData<ArchiveBorrowApi.Borrow>(); |
| | | await formApi.resetForm(); |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="å½è¿ç»è®°" class="w-1/2"> |
| | | <div class="mx-3 mb-3 text-sm text-gray-600"> |
| | | <p> |
| | | åé
åå·ï¼{{ current?.no }}ãæ¡£æ¡ï¼{{ current?.documentNo }} |
| | | {{ current?.documentTitle }} |
| | | </p> |
| | | <p> |
| | | åé
人ï¼{{ current?.borrowerName }}ãå½è¿æéï¼{{ current?.returnDeadline }} |
| | | </p> |
| | | </div> |
| | | <Form class="mx-3" /> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ArchiveCategoryApi } from '#/api/archive/category'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { handleTree } from '@vben/utils'; |
| | | |
| | | import { getCategoryList } from '#/api/archive/category'; |
| | | |
| | | /** æ°å¢/ä¿®æ¹ç表å */ |
| | | export function useFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | component: 'Input', |
| | | fieldName: 'id', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'parentId', |
| | | label: 'ä¸çº§åç±»', |
| | | component: 'ApiTreeSelect', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: async () => { |
| | | const data = await getCategoryList(); |
| | | data.unshift({ |
| | | id: 0, |
| | | name: '顶级åç±»', |
| | | } as ArchiveCategoryApi.Category); |
| | | return handleTree(data); |
| | | }, |
| | | fieldNames: { label: 'name', value: 'id', children: 'children' }, |
| | | placeholder: 'è¯·éæ©ä¸çº§åç±»', |
| | | showSearch: true, |
| | | treeDefaultExpandAll: true, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'name', |
| | | label: 'åç±»åç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥åç±»åç§°', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'code', |
| | | label: 'åç±»ç¼ç ', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'è±æç¼ç ï¼å¦ member/research/finance', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'defaultSecretLevel', |
| | | label: 'é»è®¤å¯çº§', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.ARCHIVE_SECRET_LEVEL, 'number'), |
| | | placeholder: '该åç±»ä¸æ¡£æ¡çé»è®¤å¯çº§', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'sort', |
| | | label: 'æ¾ç¤ºæåº', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | min: 0, |
| | | placeholder: 'æ°åè¶å°è¶é å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.ARCHIVE_CATEGORY_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'description', |
| | | label: 'å类说æ', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | rows: 2, |
| | | placeholder: '请è¾å
¥å类说æ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | rows: 2, |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'name', |
| | | label: 'åç±»åç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥åç±»åç§°', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.ARCHIVE_CATEGORY_STATUS, 'number'), |
| | | allowClear: true, |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** è¡¨æ ¼åé
ç½® */ |
| | | export function useGridColumns(): VxeTableGridOptions<ArchiveCategoryApi.Category>['columns'] { |
| | | return [ |
| | | { |
| | | field: 'name', |
| | | title: 'åç±»åç§°', |
| | | treeNode: true, |
| | | }, |
| | | { |
| | | field: 'code', |
| | | title: 'åç±»ç¼ç ', |
| | | minWidth: 130, |
| | | }, |
| | | { |
| | | field: 'defaultSecretLevel', |
| | | title: 'é»è®¤å¯çº§', |
| | | minWidth: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.ARCHIVE_SECRET_LEVEL }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'sort', |
| | | title: 'æåº', |
| | | minWidth: 80, |
| | | }, |
| | | { |
| | | field: 'description', |
| | | title: 'å类说æ', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | minWidth: 90, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.ARCHIVE_CATEGORY_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | minWidth: 170, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | field: 'actions', |
| | | title: 'æä½', |
| | | width: 250, |
| | | fixed: 'right', |
| | | slots: { |
| | | default: 'actions', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ArchiveCategoryApi } from '#/api/archive/category'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteCategory, |
| | | getCategoryList, |
| | | } from '#/api/archive/category'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | /** æ¡£æ¡åç±»ï¼ä¹¦æ¶ï¼ */ |
| | | defineOptions({ name: 'ArchiveCategory' }); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** 忢æ å½¢å±å¼/æ¶ç¼©ç¶æ */ |
| | | const isExpanded = ref(false); |
| | | function handleExpand() { |
| | | isExpanded.value = !isExpanded.value; |
| | | gridApi.grid.setAllTreeExpand(isExpanded.value); |
| | | } |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** å建åç±» */ |
| | | function handleCreate() { |
| | | formModalApi.setData(null).open(); |
| | | } |
| | | |
| | | /** æ·»å ä¸çº§åç±» */ |
| | | function handleAppend(row: ArchiveCategoryApi.Category) { |
| | | formModalApi.setData({ parentId: row.id }).open(); |
| | | } |
| | | |
| | | /** ç¼è¾åç±» */ |
| | | function handleEdit(row: ArchiveCategoryApi.Category) { |
| | | formModalApi.setData(row).open(); |
| | | } |
| | | |
| | | /** å é¤åç±» */ |
| | | async function handleDelete(row: ArchiveCategoryApi.Category) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting', [row.name]), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await deleteCategory(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', [row.name])); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | pagerConfig: { |
| | | enabled: false, |
| | | }, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async (_, formValues) => { |
| | | return await getCategoryList(formValues); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | treeConfig: { |
| | | parentField: 'parentId', |
| | | rowField: 'id', |
| | | transform: true, |
| | | expandAll: true, |
| | | reserve: true, |
| | | }, |
| | | } as VxeTableGridOptions<ArchiveCategoryApi.Category>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <Grid table-title="æ¡£æ¡åç±»ï¼ä¹¦æ¶ï¼"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['æ¡£æ¡åç±»']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['archive:category:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: isExpanded ? 'æ¶ç¼©' : 'å±å¼', |
| | | type: 'primary', |
| | | onClick: handleExpand, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'æ°å¢ä¸çº§', |
| | | type: 'link', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['archive:category:create'], |
| | | onClick: handleAppend.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['archive:category:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['archive:category:delete'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.name]), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { ArchiveCategoryApi } from '#/api/archive/category'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createCategory, |
| | | getCategory, |
| | | updateCategory, |
| | | } from '#/api/archive/category'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | defineOptions({ name: 'ArchiveCategoryForm' }); |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const formData = ref<ArchiveCategoryApi.Category>(); |
| | | |
| | | const getTitle = computed(() => { |
| | | if (formData.value?.id) { |
| | | return $t('ui.actionTitle.edit', ['æ¡£æ¡åç±»']); |
| | | } |
| | | return formData.value?.parentId |
| | | ? 'æ°å¢ä¸çº§åç±»' |
| | | : $t('ui.actionTitle.create', ['æ¡£æ¡åç±»']); |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | labelWidth: 90, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: useFormSchema(), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = (await formApi.getValues()) as ArchiveCategoryApi.Category; |
| | | try { |
| | | await (formData.value?.id |
| | | ? updateCategory({ ...data, id: formData.value.id }) |
| | | : createCategory(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | return; |
| | | } |
| | | let data = modalApi.getData<ArchiveCategoryApi.Category>(); |
| | | // æ°å¢ï¼é¡¶çº§ / ä¸çº§ï¼æ¶åªå¸¦ parentId ä½ä¸ºé»è®¤å¼ |
| | | if (!data || !data.id) { |
| | | formData.value = data ?? undefined; |
| | | await formApi.resetForm(); |
| | | if (data?.parentId !== undefined) { |
| | | await formApi.setValues({ parentId: data.parentId }); |
| | | } |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | data = await getCategory(data.id); |
| | | formData.value = data; |
| | | await formApi.setValues(data); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal class="w-2/5" :title="getTitle"> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ArchiveDocumentApi } from '#/api/archive/document'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { handleTree } from '@vben/utils'; |
| | | |
| | | import { getCategoryList } from '#/api/archive/category'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'edit'; |
| | | |
| | | /** æ¡£æ¡è¡¨åé
ç½® */ |
| | | export function useFormSchema(formType: FormType): VbenFormSchema[] { |
| | | const readonly = formType === 'detail'; |
| | | // æ¡£æ¡ç¼å·ãæ¥æºãç¶æã彿¡£äººç±å端维æ¤ï¼ä»
ç¼è¾/详æ
å¯è§ |
| | | const showBackendFields = formType !== 'create'; |
| | | |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'no', |
| | | label: 'æ¡£æ¡ç¼å·', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => showBackendFields, |
| | | }, |
| | | componentProps: { |
| | | disabled: true, |
| | | placeholder: 'ç³»ç»èªå¨çæ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'æ¡£æ¡ç¶æ', |
| | | component: 'Select', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => showBackendFields, |
| | | }, |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.ARCHIVE_DOCUMENT_STATUS, 'number'), |
| | | disabled: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'title', |
| | | label: 'æ¡£æ¡æ é¢', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | disabled: readonly, |
| | | placeholder: '请è¾å
¥æ¡£æ¡æ é¢', |
| | | }, |
| | | rules: 'required', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'categoryId', |
| | | label: 'æ¡£æ¡åç±»', |
| | | component: 'ApiTreeSelect', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: async () => handleTree(await getCategoryList({ status: 0 })), |
| | | fieldNames: { label: 'name', value: 'id', children: 'children' }, |
| | | placeholder: 'è¯·éæ©æ¡£æ¡åç±»', |
| | | showSearch: true, |
| | | treeDefaultExpandAll: true, |
| | | disabled: readonly, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'secretLevel', |
| | | label: 'å¯çº§', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.ARCHIVE_SECRET_LEVEL, 'number'), |
| | | placeholder: 'è¯·éæ©å¯çº§', |
| | | disabled: readonly, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'carrierType', |
| | | label: 'è½½ä½', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.ARCHIVE_CARRIER_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©è½½ä½', |
| | | disabled: readonly, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'archiveDate', |
| | | label: '彿¡£æ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | allowClear: true, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | placeholder: 'è¯·éæ©å½æ¡£æ¥æ', |
| | | disabled: readonly, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'shelfNo', |
| | | label: '书æ¶å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: 'å¦ Aæ¶', |
| | | disabled: readonly, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'layerNo', |
| | | label: 'å±å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: 'å¦ 2å±', |
| | | disabled: readonly, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'columnNo', |
| | | label: 'åå·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: 'å¦ 3å', |
| | | disabled: readonly, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'boxNo', |
| | | label: 'åå·/æ¡£æ¡çå·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: 'å¦ æ¡£æ¡ç-001', |
| | | disabled: readonly, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'storePlace', |
| | | label: 'åæ¾ä½ç½®', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: 'å¦ 3å·åºæ¿Aæ¶2å±', |
| | | disabled: readonly, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'tags', |
| | | label: 'å
³é®è¯', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: 'å¤ä¸ªå
³é®è¯ç¨è±æéå·åéï¼ä¾¿äºæ£ç´¢', |
| | | disabled: readonly, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'summary', |
| | | label: 'æè¦', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | rows: 2, |
| | | placeholder: '请è¾å
¥æè¦', |
| | | disabled: readonly, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'sourceType', |
| | | label: 'æ¥æºç±»å', |
| | | component: 'Select', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => showBackendFields, |
| | | }, |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.ARCHIVE_SOURCE_TYPE, 'number'), |
| | | disabled: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'sourceNo', |
| | | label: 'æ¥æºåå·', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => showBackendFields, |
| | | }, |
| | | componentProps: { |
| | | disabled: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'archivistName', |
| | | label: '彿¡£äºº', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => showBackendFields, |
| | | }, |
| | | componentProps: { |
| | | disabled: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | rows: 2, |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | disabled: readonly, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'blobIds', |
| | | label: 'çµåæä»¶', |
| | | component: 'FileUpload', |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | maxNumber: 10, |
| | | multiple: true, |
| | | showDescription: !readonly, |
| | | disabled: readonly, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'no', |
| | | label: 'æ¡£æ¡ç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥æ¡£æ¡ç¼å·', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'title', |
| | | label: 'æ¡£æ¡æ é¢', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥æ¡£æ¡æ é¢', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'categoryId', |
| | | label: 'æ¡£æ¡åç±»', |
| | | component: 'ApiTreeSelect', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: async () => handleTree(await getCategoryList({ status: 0 })), |
| | | fieldNames: { label: 'name', value: 'id', children: 'children' }, |
| | | placeholder: 'è¯·éæ©æ¡£æ¡åç±»', |
| | | showSearch: true, |
| | | treeDefaultExpandAll: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'secretLevel', |
| | | label: 'å¯çº§', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.ARCHIVE_SECRET_LEVEL, 'number'), |
| | | allowClear: true, |
| | | placeholder: 'è¯·éæ©å¯çº§', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'carrierType', |
| | | label: 'è½½ä½', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.ARCHIVE_CARRIER_TYPE, 'number'), |
| | | allowClear: true, |
| | | placeholder: 'è¯·éæ©è½½ä½', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'sourceType', |
| | | label: 'æ¥æºç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.ARCHIVE_SOURCE_TYPE, 'number'), |
| | | allowClear: true, |
| | | placeholder: 'è¯·éæ©æ¥æºç±»å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'sourceNo', |
| | | label: 'æ¥æºåå·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥æ¥æºåå·', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'tags', |
| | | label: 'å
³é®è¯', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥å
³é®è¯', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'æ¡£æ¡ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.ARCHIVE_DOCUMENT_STATUS, 'number'), |
| | | allowClear: true, |
| | | placeholder: 'è¯·éæ©æ¡£æ¡ç¶æ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'archiveDate', |
| | | label: '彿¡£æ¥æ', |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | allowClear: true, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | placeholder: ['å¼å§æ¥æ', 'ç»ææ¥æ'], |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** è¡¨æ ¼åé
ç½® */ |
| | | export function useGridColumns(): VxeTableGridOptions<ArchiveDocumentApi.Document>['columns'] { |
| | | return [ |
| | | { |
| | | field: 'no', |
| | | title: 'æ¡£æ¡ç¼å·', |
| | | minWidth: 170, |
| | | }, |
| | | { |
| | | field: 'title', |
| | | title: 'æ¡£æ¡æ é¢', |
| | | minWidth: 220, |
| | | }, |
| | | { |
| | | field: 'categoryName', |
| | | title: 'æ¡£æ¡åç±»', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'secretLevel', |
| | | title: 'å¯çº§', |
| | | minWidth: 90, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.ARCHIVE_SECRET_LEVEL }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'carrierType', |
| | | title: 'è½½ä½', |
| | | minWidth: 110, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.ARCHIVE_CARRIER_TYPE }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'sourceType', |
| | | title: 'æ¥æº', |
| | | minWidth: 90, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.ARCHIVE_SOURCE_TYPE }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'sourceNo', |
| | | title: 'æ¥æºåå·', |
| | | minWidth: 160, |
| | | }, |
| | | { |
| | | field: 'storePlace', |
| | | title: 'åæ¾ä½ç½®', |
| | | minWidth: 170, |
| | | }, |
| | | { |
| | | field: 'archiveDate', |
| | | title: '彿¡£æ¥æ', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'æ¡£æ¡ç¶æ', |
| | | minWidth: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.ARCHIVE_DOCUMENT_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'hasAttachment', |
| | | title: 'çµåæä»¶', |
| | | minWidth: 100, |
| | | slots: { default: 'hasAttachment' }, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 200, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ArchiveDocumentApi } from '#/api/archive/document'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteDocument, |
| | | exportDocument, |
| | | getDocumentPage, |
| | | } from '#/api/archive/document'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | /** æ¡£æ¡ç®¡ç */ |
| | | defineOptions({ name: 'ArchiveDocument' }); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** æ°å¢æ¡£æ¡ */ |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | /** ç¼è¾æ¡£æ¡ */ |
| | | function handleEdit(row: ArchiveDocumentApi.Document) { |
| | | formModalApi.setData({ formType: 'edit', id: row.id }).open(); |
| | | } |
| | | |
| | | /** æ¥çæ¡£æ¡è¯¦æ
*/ |
| | | function handleDetail(row: ArchiveDocumentApi.Document) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | /** å 餿¡£æ¡ */ |
| | | async function handleDelete(row: ArchiveDocumentApi.Document) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting', [row.title]), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await deleteDocument(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', [row.title])); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** å¯¼åº Excel */ |
| | | async function handleExport() { |
| | | const data = await exportDocument(await gridApi.formApi.getValues()); |
| | | downloadFileFromBlobPart({ fileName: 'æ¡£æ¡.xls', source: data }); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getDocumentPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<ArchiveDocumentApi.Document>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <Grid table-title="æ¡£æ¡ç®¡ç"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['æ¡£æ¡']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['archive:document:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: '导åº', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['archive:document:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #hasAttachment="{ row }"> |
| | | <Tag :color="row.hasAttachment ? 'success' : 'default'"> |
| | | {{ row.hasAttachment ? 'æ' : 'æ ' }} |
| | | </Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.detail'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['archive:document:query'], |
| | | onClick: handleDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['archive:document:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['archive:document:delete'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.no]), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { ArchiveDocumentApi } from '#/api/archive/document'; |
| | | |
| | | import type { FormType } from '../data'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { $t } from '@vben/locales'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createDocument, |
| | | getDocument, |
| | | updateDocument, |
| | | } from '#/api/archive/document'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | defineOptions({ name: 'ArchiveDocumentForm' }); |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const formType = ref<FormType>('create'); |
| | | |
| | | const getTitle = computed(() => { |
| | | if (formType.value === 'create') { |
| | | return $t('ui.actionTitle.create', ['æ¡£æ¡']); |
| | | } |
| | | if (formType.value === 'edit') { |
| | | return $t('ui.actionTitle.edit', ['æ¡£æ¡']); |
| | | } |
| | | return 'æ¡£æ¡è¯¦æ
'; |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | labelWidth: 120, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'vertical', |
| | | schema: [], |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | /** åªæäº¤å端 SaveReqVO æ¥åçåæ®µï¼é¿å
åä¼ no/status/source* çåªè¯»å段 */ |
| | | async function buildSubmitData() { |
| | | const values = (await formApi.getValues()) as ArchiveDocumentApi.Document; |
| | | return { |
| | | id: values.id, |
| | | title: values.title, |
| | | categoryId: values.categoryId, |
| | | secretLevel: values.secretLevel, |
| | | carrierType: values.carrierType, |
| | | shelfNo: values.shelfNo, |
| | | layerNo: values.layerNo, |
| | | columnNo: values.columnNo, |
| | | boxNo: values.boxNo, |
| | | storePlace: values.storePlace, |
| | | archiveDate: values.archiveDate, |
| | | summary: values.summary, |
| | | tags: values.tags, |
| | | remark: values.remark, |
| | | // é件为å
¨éæ¿æ¢è¯ä¹ï¼æ¸
空å
¨é¨éä»¶æ¶å¿
é¡»æ¾å¼ä¼ 空æ°ç» |
| | | blobIds: values.blobIds ?? [], |
| | | } as ArchiveDocumentApi.Document; |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = await buildSubmitData(); |
| | | try { |
| | | await (formType.value === 'create' |
| | | ? createDocument(data) |
| | | : updateDocument(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | formApi.setState({ schema: useFormSchema(formType.value) }); |
| | | formApi.setDisabled(formType.value === 'detail'); |
| | | if (data?.id) { |
| | | modalApi.lock(); |
| | | try { |
| | | const detail = await getDocument(data.id); |
| | | // å·²æé件转æä¸ä¼ ç»ä»¶å¯è¯å«çæä»¶å¯¹è±¡ï¼æäº¤æ¶æ valueKey=id æå为 blobId |
| | | const blobIds = (detail.attachmentList ?? []).map((item) => ({ |
| | | uid: String(item.id), |
| | | name: item.name || '', |
| | | url: item.url || '', |
| | | status: 'done', |
| | | id: item.id, |
| | | })); |
| | | await formApi.setValues({ ...detail, blobIds }, false); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | } else { |
| | | await formApi.resetForm(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | :title="getTitle" |
| | | class="w-3/4" |
| | | :close-on-click-modal="false" |
| | | :show-confirm-button="formType !== 'detail'" |
| | | > |
| | | <Form class="mx-3" /> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { EChartsOption } from '@vben/plugins/echarts'; |
| | | |
| | | import type { ArchiveStatApi } from '#/api/archive/stat'; |
| | | |
| | | /** é¡¶é¨ç»è®¡å¡ç */ |
| | | export interface StatCard { |
| | | key: keyof ArchiveStatApi.StatOverview; |
| | | label: string; |
| | | color: string; |
| | | } |
| | | |
| | | export const STAT_CARDS: StatCard[] = [ |
| | | { key: 'totalCount', label: 'æ¡£æ¡æ»æ°', color: '#1677ff' }, |
| | | { key: 'inStockCount', label: 'å¨åºæ¡£æ¡', color: '#52c41a' }, |
| | | { key: 'borrowedCount', label: 'ååºä¸æ¡£æ¡', color: '#faad14' }, |
| | | { key: 'overdueCount', label: '龿æªè¿', color: '#ff4d4f' }, |
| | | ]; |
| | | |
| | | /** éç¨é¥¼å¾é
ç½® */ |
| | | export function buildPieOption( |
| | | title: string, |
| | | rows: { name: string; value: number }[], |
| | | ): EChartsOption { |
| | | return { |
| | | title: { text: title, left: 'center', textStyle: { fontSize: 14 } }, |
| | | tooltip: { trigger: 'item' }, |
| | | legend: { orient: 'vertical', left: 'left', top: 'middle' }, |
| | | series: [ |
| | | { |
| | | name: title, |
| | | type: 'pie', |
| | | radius: ['40%', '65%'], |
| | | center: ['60%', '55%'], |
| | | data: rows, |
| | | emphasis: { |
| | | itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0, 0, 0, 0.5)' }, |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | } |
| | | |
| | | /** éç¨æ±ç¶å¾é
ç½® */ |
| | | export function buildBarOption( |
| | | title: string, |
| | | rows: { name: string; value: number }[], |
| | | ): EChartsOption { |
| | | return { |
| | | title: { text: title, left: 'center', textStyle: { fontSize: 14 } }, |
| | | tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, |
| | | grid: { left: 20, right: 20, bottom: 20, containLabel: true }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: rows.map((item) => item.name), |
| | | axisTick: { alignWithLabel: true }, |
| | | }, |
| | | yAxis: { type: 'value', minInterval: 1 }, |
| | | series: [ |
| | | { |
| | | name: title, |
| | | type: 'bar', |
| | | barMaxWidth: 48, |
| | | data: rows.map((item) => item.value), |
| | | label: { show: true, position: 'top' }, |
| | | }, |
| | | ], |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { EchartsUIType } from '@vben/plugins/echarts'; |
| | | |
| | | import type { ArchiveStatApi } from '#/api/archive/stat'; |
| | | |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap, Page } from '@vben/common-ui'; |
| | | import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | | import { getArchiveStatOverview } from '#/api/archive/stat'; |
| | | |
| | | import { buildBarOption, buildPieOption, STAT_CARDS } from './data'; |
| | | |
| | | /** æ¡£æ¡ç»è®¡ */ |
| | | defineOptions({ name: 'ArchiveStat' }); |
| | | |
| | | const loading = ref(false); |
| | | const overview = ref<ArchiveStatApi.StatOverview>({}); |
| | | |
| | | const categoryChartRef = ref<EchartsUIType>(); |
| | | const secretChartRef = ref<EchartsUIType>(); |
| | | const carrierChartRef = ref<EchartsUIType>(); |
| | | |
| | | const { renderEcharts: renderCategory } = useEcharts(categoryChartRef); |
| | | const { renderEcharts: renderSecret } = useEcharts(secretChartRef); |
| | | const { renderEcharts: renderCarrier } = useEcharts(carrierChartRef); |
| | | |
| | | /** å¡çåå¼ */ |
| | | const cards = computed(() => |
| | | STAT_CARDS.map((card) => ({ |
| | | ...card, |
| | | value: (overview.value[card.key] as number | undefined) ?? 0, |
| | | })), |
| | | ); |
| | | |
| | | async function loadData() { |
| | | loading.value = true; |
| | | try { |
| | | const data = await getArchiveStatOverview(); |
| | | overview.value = data; |
| | | await Promise.all([ |
| | | renderCategory( |
| | | buildPieOption( |
| | | 'æåç±»åå¸', |
| | | (data.byCategory ?? []).map((item) => ({ |
| | | name: item.name ?? 'æªå½å', |
| | | value: item.total ?? 0, |
| | | })), |
| | | ), |
| | | ), |
| | | renderSecret( |
| | | buildBarOption( |
| | | 'æå¯çº§åå¸', |
| | | (data.bySecretLevel ?? []).map((item) => ({ |
| | | name: item.name ?? 'æªè®¾ç½®', |
| | | value: item.total ?? 0, |
| | | })), |
| | | ), |
| | | ), |
| | | renderCarrier( |
| | | buildPieOption( |
| | | 'æè½½ä½åå¸', |
| | | (data.byCarrierType ?? []).map((item) => ({ |
| | | name: item.name ?? 'æªè®¾ç½®', |
| | | value: item.total ?? 0, |
| | | })), |
| | | ), |
| | | ), |
| | | ]); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | onMounted(loadData); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <Spin :spinning="loading" tip="å è½½ä¸..."> |
| | | <div class="space-y-4"> |
| | | <!-- ç»è®¡å¡ç --> |
| | | <div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4"> |
| | | <ContentWrap |
| | | v-for="card in cards" |
| | | :key="card.key" |
| | | class="flex items-center justify-between" |
| | | > |
| | | <div> |
| | | <p class="text-sm text-gray-500">{{ card.label }}</p> |
| | | <p |
| | | class="mt-1 text-2xl font-semibold" |
| | | :style="{ color: card.color }" |
| | | > |
| | | {{ card.value }} |
| | | </p> |
| | | </div> |
| | | <span |
| | | class="inline-block h-10 w-1 rounded" |
| | | :style="{ backgroundColor: card.color }" |
| | | /> |
| | | </ContentWrap> |
| | | </div> |
| | | |
| | | <!-- åå¸å¾è¡¨ --> |
| | | <div class="grid grid-cols-1 gap-4 xl:grid-cols-2"> |
| | | <ContentWrap> |
| | | <EchartsUI class="h-72 w-full" ref="categoryChartRef" /> |
| | | </ContentWrap> |
| | | <ContentWrap> |
| | | <EchartsUI class="h-72 w-full" ref="secretChartRef" /> |
| | | </ContentWrap> |
| | | </div> |
| | | <ContentWrap> |
| | | <EchartsUI class="h-72 w-full" ref="carrierChartRef" /> |
| | | </ContentWrap> |
| | | </div> |
| | | </Spin> |
| | | </Page> |
| | | </template> |
| | |
| | | <script lang="ts" setup> |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | import { computed } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | |
| | | import { Card, Col, Row } from 'ant-design-vue'; |
| | | import { Col, Row } from 'ant-design-vue'; |
| | | |
| | | import { getHomeSummary } from '#/api/mes/home'; |
| | | |
| | | import { defaultSummary } from './data'; |
| | | import AiInsights from './modules/ai-insights.vue'; |
| | | import AlertPanel from './modules/alert-panel.vue'; |
| | | import BizCards from './modules/biz-cards.vue'; |
| | | import KpiCards from './modules/kpi-cards.vue'; |
| | | import ProductionTrend from './modules/production-trend.vue'; |
| | | import QuickActions from './modules/quick-actions.vue'; |
| | | import RecentApplications from './modules/recent-applications.vue'; |
| | | import TodoPanel from './modules/todo-panel.vue'; |
| | | |
| | | defineOptions({ name: 'DashboardAnalytics' }); |
| | | |
| | | // vite.config.ts 注å
¥çå
¬å¸ Logo è·¯å¾ |
| | | declare const __APP_LOGO__: string; |
| | | const brandLogo = __APP_LOGO__ || '/logos/JSSM.svg'; |
| | | |
| | | const router = useRouter(); |
| | | const summary = ref(defaultSummary); |
| | | |
| | | const greeting = computed(() => { |
| | | const hour = new Date().getHours(); |
| | |
| | | function handleNavigate(name: string) { |
| | | router.push({ name }); |
| | | } |
| | | |
| | | async function loadSummary() { |
| | | summary.value = await getHomeSummary(); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadSummary(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <!-- æ¬¢è¿æ --> |
| | | <div class="glass-card mb-6 flex items-center justify-between rounded-2xl p-6"> |
| | | <div> |
| | | <h1 class="bg-gradient-to-r from-blue-600 via-purple-600 to-pink-500 bg-clip-text text-2xl font-bold text-transparent dark:from-blue-400 dark:via-purple-400 dark:to-pink-400"> |
| | | {{ greeting }}ï¼æ¬¢è¿åæ¥ |
| | | </h1> |
| | | <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ today }}</p> |
| | | </div> |
| | | <div class="hidden items-center gap-2 sm:flex"> |
| | | <div class="flex items-center gap-1.5 rounded-full bg-emerald-50 px-3 py-1.5 text-xs font-medium text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-400"> |
| | | <span class="relative flex size-2"> |
| | | <span class="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-75" /> |
| | | <span class="relative inline-flex size-2 rounded-full bg-emerald-500" /> |
| | | </span> |
| | | <!-- æ¬¢è¿æ ï¼èéåº + éè²æè¾¹ --> |
| | | <div class="elite-hero mb-6"> |
| | | <img alt="" class="elite-hero-mark" :src="brandLogo" /> |
| | | <div class="relative z-10 flex flex-wrap items-center justify-between gap-4"> |
| | | <div> |
| | | <h1 class="elite-hero-title">{{ greeting }}ï¼æ¬¢è¿åæ¥</h1> |
| | | <p class="elite-hero-date">{{ today }}</p> |
| | | </div> |
| | | <div class="elite-status"> |
| | | <span class="elite-status-dot"></span> |
| | | ç³»ç»è¿è¡ä¸ |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- MES ç产 KPI --> |
| | | <KpiCards :summary="summary" @navigate="handleNavigate" /> |
| | | <!-- ç³»ç»ææ --> |
| | | <KpiCards @navigate="handleNavigate" /> |
| | | |
| | | <!-- ERP/WMS/BPM ç»è¥ KPI --> |
| | | <BizCards @navigate="handleNavigate" /> |
| | | |
| | | <!-- AI æºè½åæ --> |
| | | <AiInsights |
| | | :active-order-count="summary.workOrderActiveCount" |
| | | @navigate="handleNavigate" |
| | | /> |
| | | |
| | | <!-- ç产è¶å¿ + é¢è¦é¢æ¿ --> |
| | | <!-- å¾
å + å¿«æ·å
¥å£ --> |
| | | <Row :gutter="16" class="mb-4"> |
| | | <Col :lg="14" :md="24" :sm="24" :xl="14" :xs="24" class="mb-4"> |
| | | <div class="glass-card rounded-2xl p-5"> |
| | | <h3 class="mb-4 text-base font-semibold text-gray-800 dark:text-gray-100"> |
| | | ç产è¶å¿ï¼è¿7å¤©ï¼ |
| | | </h3> |
| | | <ProductionTrend /> |
| | | </div> |
| | | <TodoPanel @navigate="handleNavigate" /> |
| | | </Col> |
| | | <Col :lg="10" :md="24" :sm="24" :xl="10" :xs="24" class="mb-4"> |
| | | <AlertPanel |
| | | :repair-active-count="summary.repairActiveCount" |
| | | @navigate="handleNavigate" |
| | | /> |
| | | </Col> |
| | | </Row> |
| | | |
| | | <!-- å¿«æ·æä½ --> |
| | | <Row :gutter="16" class="mb-4"> |
| | | <Col :span="24"> |
| | | <QuickActions @navigate="handleNavigate" /> |
| | | </Col> |
| | | </Row> |
| | | |
| | | <!-- æè¿è¯¾é¢ç³æ¥ --> |
| | | <RecentApplications @navigate="handleNavigate" /> |
| | | </Page> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .glass-card { |
| | | background: linear-gradient(135deg, rgba(255,255,255,0.8), rgba(255,255,255,0.5)); |
| | | backdrop-filter: blur(20px); |
| | | -webkit-backdrop-filter: blur(20px); |
| | | border: 1px solid rgba(255,255,255,0.6); |
| | | box-shadow: |
| | | 0 8px 32px rgba(0,0,0,0.04), |
| | | 0 2px 8px rgba(0,0,0,0.02), |
| | | inset 0 1px 0 rgba(255,255,255,0.8); |
| | | /* ===== æ¬¢è¿æ ===== */ |
| | | .elite-hero { |
| | | position: relative; |
| | | overflow: hidden; |
| | | border-radius: 12px; |
| | | padding: 30px 34px; |
| | | background: |
| | | radial-gradient(circle at 88% 12%, rgba(212, 175, 55, 0.08), transparent 50%), |
| | | linear-gradient(135deg, #123156 0%, #0f2440 58%, #0b1c33 100%); |
| | | border: 1px solid rgba(212, 175, 55, 0.24); |
| | | box-shadow: 0 4px 18px rgba(15, 36, 64, 0.12); |
| | | } |
| | | |
| | | .dark .glass-card { |
| | | background: linear-gradient(135deg, rgba(30,30,50,0.7), rgba(20,20,40,0.5)); |
| | | border: 1px solid rgba(255,255,255,0.08); |
| | | box-shadow: |
| | | 0 8px 32px rgba(0,0,0,0.3), |
| | | 0 2px 8px rgba(0,0,0,0.2), |
| | | inset 0 1px 0 rgba(255,255,255,0.05); |
| | | /* å³ä¾§æ·¡æ·¡çå¾½æ æ°´å° */ |
| | | .elite-hero-mark { |
| | | position: absolute; |
| | | top: 50%; |
| | | right: 168px; |
| | | width: 118px; |
| | | height: 118px; |
| | | pointer-events: none; |
| | | opacity: 0.05; |
| | | transform: translateY(-50%); |
| | | } |
| | | |
| | | .elite-hero-title { |
| | | margin: 0; |
| | | font-size: 22px; |
| | | font-weight: 600; |
| | | letter-spacing: 2px; |
| | | color: #f5f0e4; |
| | | } |
| | | |
| | | .elite-hero-date { |
| | | margin: 8px 0 0; |
| | | font-size: 13px; |
| | | letter-spacing: 1px; |
| | | color: rgba(212, 175, 55, 0.85); |
| | | } |
| | | |
| | | .elite-status { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 7px 16px; |
| | | font-size: 12.5px; |
| | | letter-spacing: 1px; |
| | | color: #f0e2b8; |
| | | background: rgba(212, 175, 55, 0.12); |
| | | border: 1px solid rgba(212, 175, 55, 0.38); |
| | | border-radius: 999px; |
| | | } |
| | | |
| | | /* éæç¶æç¹ï¼ä¸åèå²å¨ç» */ |
| | | .elite-status-dot { |
| | | display: inline-flex; |
| | | width: 7px; |
| | | height: 7px; |
| | | border-radius: 999px; |
| | | background: #d4af37; |
| | | } |
| | | </style> |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesHomeApi } from '#/api/mes/home'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | import { onMounted, ref } from 'vue'; |
| | | |
| | | import { CountTo } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Col, Row } from 'ant-design-vue'; |
| | | |
| | | defineOptions({ name: 'DashboardKpiCards' }); |
| | | import { getTaskTodoPage } from '#/api/bpm/task'; |
| | | import { getReimbursementPage } from '#/api/finance/reimbursement'; |
| | | import { getApplicationPage } from '#/api/research/application'; |
| | | import { getGuidelinePage } from '#/api/research/guideline'; |
| | | |
| | | const props = defineProps<{ |
| | | summary: MesHomeApi.Summary; |
| | | }>(); |
| | | defineOptions({ name: 'DashboardKpiCards' }); |
| | | |
| | | const emit = defineEmits<{ |
| | | navigate: [name: string]; |
| | | }>(); |
| | | |
| | | const qualityRate = computed(() => { |
| | | const total = |
| | | props.summary.todayQualifiedQuantity + |
| | | props.summary.todayUnqualifiedQuantity; |
| | | return total === 0 ? 0 : (props.summary.todayQualifiedQuantity / total) * 100; |
| | | const todoCount = ref(0); |
| | | const applicationCount = ref(0); |
| | | const reimbursementCount = ref(0); |
| | | const guidelineCount = ref(0); |
| | | |
| | | async function loadTodo() { |
| | | try { |
| | | const result = await getTaskTodoPage({ pageNo: 1, pageSize: 1 }); |
| | | todoCount.value = result.total ?? 0; |
| | | } catch { |
| | | /* æ æéææ¥å£å¼å¸¸ï¼ä¿æ 0 */ |
| | | } |
| | | } |
| | | |
| | | async function loadApplication() { |
| | | try { |
| | | const result = await getApplicationPage({ pageNo: 1, pageSize: 1 }); |
| | | applicationCount.value = result.total ?? 0; |
| | | } catch { |
| | | /* æ æéææ¥å£å¼å¸¸ï¼ä¿æ 0 */ |
| | | } |
| | | } |
| | | |
| | | async function loadReimbursement() { |
| | | try { |
| | | const result = await getReimbursementPage({ pageNo: 1, pageSize: 1 }); |
| | | reimbursementCount.value = result.total ?? 0; |
| | | } catch { |
| | | /* æ æéææ¥å£å¼å¸¸ï¼ä¿æ 0 */ |
| | | } |
| | | } |
| | | |
| | | async function loadGuideline() { |
| | | try { |
| | | const result = await getGuidelinePage({ pageNo: 1, pageSize: 1 }); |
| | | guidelineCount.value = result.total ?? 0; |
| | | } catch { |
| | | /* æ æéææ¥å£å¼å¸¸ï¼ä¿æ 0 */ |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadTodo(); |
| | | loadApplication(); |
| | | loadReimbursement(); |
| | | loadGuideline(); |
| | | }); |
| | | |
| | | const equipmentRate = computed(() => { |
| | | if (props.summary.machineryTotal === 0) return 0; |
| | | return (props.summary.machineryProducing / props.summary.machineryTotal) * 100; |
| | | }); |
| | | |
| | | /** 3D å¾æææ */ |
| | | interface TiltState { x: number; y: number } |
| | | |
| | | const tilts = ref<Record<string, TiltState>>({}); |
| | | |
| | | function onMouseMove(e: MouseEvent, id: string) { |
| | | const el = e.currentTarget as HTMLElement; |
| | | const rect = el.getBoundingClientRect(); |
| | | const x = ((e.clientX - rect.left) / rect.width - 0.5) * 2; |
| | | const y = ((e.clientY - rect.top) / rect.height - 0.5) * 2; |
| | | tilts.value[id] = { x: x * -6, y: y * 6 }; |
| | | } |
| | | |
| | | function onMouseLeave(id: string) { |
| | | tilts.value[id] = { x: 0, y: 0 }; |
| | | } |
| | | |
| | | function tiltStyle(id: string) { |
| | | const t = tilts.value[id] ?? { x: 0, y: 0 }; |
| | | return { |
| | | transform: `perspective(600px) rotateX(${t.y}deg) rotateY(${t.x}deg)`, |
| | | transition: t.x === 0 && t.y === 0 ? 'transform 0.5s ease' : 'transform 0.1s ease', |
| | | }; |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Row :gutter="16"> |
| | | <!-- ç产订å --> |
| | | <!-- æçå¾
å --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('order')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'order')" |
| | | @mouseleave="onMouseLeave('order')" |
| | | @click="emit('navigate', 'MesProWorkOrder')" |
| | | class="elite-card group cursor-pointer" |
| | | @click="emit('navigate', 'BpmTaskTodo')" |
| | | > |
| | | <div class="relative z-10 flex items-center gap-4 p-5"> |
| | | <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500 to-cyan-400 shadow-lg shadow-blue-500/25 transition-transform duration-300 group-hover:scale-110"> |
| | | <IconifyIcon class="size-6 text-white" icon="lucide:clipboard-list" /> |
| | | <div class="flex items-center gap-4 p-5"> |
| | | <div class="elite-icon-tile"> |
| | | <IconifyIcon class="size-6" icon="lucide:list-todo" /> |
| | | </div> |
| | | <div class="min-w-0 flex-1"> |
| | | <div class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">ç产订å</div> |
| | | <div class="flex items-baseline gap-1"> |
| | | <CountTo class="text-2xl font-bold leading-tight text-blue-600 dark:text-blue-400" :end-val="summary.workOrderActiveCount" :duration="1200" /> |
| | | <span class="text-xs text-gray-400">è¿è¡ä¸</span> |
| | | </div> |
| | | <div class="mt-1 flex items-center gap-1 text-xs text-gray-400"> |
| | | <span class="inline-block size-1.5 rounded-full bg-amber-400" /> å¾
æäº§ {{ summary.workOrderPrepareCount }} |
| | | <span class="mx-1">|</span> |
| | | <span class="inline-block size-1.5 rounded-full bg-emerald-400" /> 已宿 {{ summary.workOrderFinishedCount }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="absolute inset-0 -z-0 bg-gradient-to-br from-blue-500/5 to-cyan-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" /> |
| | | <div class="absolute -right-6 -top-6 size-24 rounded-full bg-blue-400/10 blur-xl transition-all duration-300 group-hover:scale-150" /> |
| | | </div> |
| | | </Col> |
| | | |
| | | <!-- 仿¥äº§é --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('output')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'output')" |
| | | @mouseleave="onMouseLeave('output')" |
| | | @click="emit('navigate', 'MesProFeedback')" |
| | | > |
| | | <div class="relative z-10 flex items-center gap-4 p-5"> |
| | | <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-emerald-500 to-green-400 shadow-lg shadow-emerald-500/25 transition-transform duration-300 group-hover:scale-110"> |
| | | <IconifyIcon class="size-6 text-white" icon="lucide:trending-up" /> |
| | | </div> |
| | | <div class="min-w-0 flex-1"> |
| | | <div class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">仿¥äº§é</div> |
| | | <div class="flex items-baseline gap-1"> |
| | | <CountTo class="text-2xl font-bold leading-tight text-emerald-600 dark:text-emerald-400" :end-val="summary.todayOutput" :duration="1200" /> |
| | | <span class="text-xs text-gray-400">ä»¶</span> |
| | | </div> |
| | | <div class="mt-1 text-xs text-gray-400"> |
| | | æ¨æ¥ {{ summary.yesterdayOutput }} ä»¶ |
| | | <span v-if="summary.yesterdayOutput > 0 && summary.todayOutput >= summary.yesterdayOutput" class="text-emerald-500">â</span> |
| | | <span v-else-if="summary.yesterdayOutput > 0" class="text-red-500">â</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="absolute inset-0 -z-0 bg-gradient-to-br from-emerald-500/5 to-green-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" /> |
| | | <div class="absolute -right-6 -top-6 size-24 rounded-full bg-emerald-400/10 blur-xl transition-all duration-300 group-hover:scale-150" /> |
| | | </div> |
| | | </Col> |
| | | |
| | | <!-- è´¨éåæ ¼ç --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('quality')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'quality')" |
| | | @mouseleave="onMouseLeave('quality')" |
| | | @click="emit('navigate', 'MesQcRecord')" |
| | | > |
| | | <div class="relative z-10 flex items-center gap-4 p-5"> |
| | | <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-orange-500 to-amber-400 shadow-lg shadow-orange-500/25 transition-transform duration-300 group-hover:scale-110"> |
| | | <IconifyIcon class="size-6 text-white" icon="lucide:shield-check" /> |
| | | </div> |
| | | <div class="min-w-0 flex-1"> |
| | | <div class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">è´¨éåæ ¼ç</div> |
| | | <div class="elite-label">æçå¾
å</div> |
| | | <div class="flex items-baseline gap-1"> |
| | | <CountTo |
| | | class="text-2xl font-bold leading-tight" |
| | | :class="qualityRate >= 95 ? 'text-emerald-600 dark:text-emerald-400' : qualityRate >= 80 ? 'text-orange-600 dark:text-orange-400' : 'text-red-600 dark:text-red-400'" |
| | | :decimals="1" :end-val="qualityRate" :duration="1200" |
| | | class="elite-value" |
| | | :class="todoCount > 0 ? 'text-[#b8901f] dark:text-[#e3c567]' : ''" |
| | | :duration="1200" |
| | | :end-val="todoCount" |
| | | /> |
| | | <span class="text-xs text-gray-400">%</span> |
| | | <span class="elite-unit">项</span> |
| | | </div> |
| | | <div class="mt-1 text-xs text-gray-400"> |
| | | åæ ¼ {{ summary.todayQualifiedQuantity }} | ä¸è¯ {{ summary.todayUnqualifiedQuantity }} |
| | | </div> |
| | | <div class="elite-meta">审æ¹ä¸å¿ · å¾
å¤çä»»å¡</div> |
| | | </div> |
| | | </div> |
| | | <div class="absolute inset-0 -z-0 bg-gradient-to-br from-orange-500/5 to-amber-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" /> |
| | | <div class="absolute -right-6 -top-6 size-24 rounded-full bg-orange-400/10 blur-xl transition-all duration-300 group-hover:scale-150" /> |
| | | </div> |
| | | </Col> |
| | | |
| | | <!-- 设å¤è¿è¡ç --> |
| | | <!-- 课é¢ç³æ¥ --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('equip')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'equip')" |
| | | @mouseleave="onMouseLeave('equip')" |
| | | @click="emit('navigate', 'MesDvMachinery')" |
| | | class="elite-card group cursor-pointer" |
| | | @click="emit('navigate', 'ResearchApplication')" |
| | | > |
| | | <div class="relative z-10 flex items-center gap-4 p-5"> |
| | | <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-violet-500 to-purple-400 shadow-lg shadow-violet-500/25 transition-transform duration-300 group-hover:scale-110"> |
| | | <IconifyIcon class="size-6 text-white" icon="lucide:cpu" /> |
| | | <div class="flex items-center gap-4 p-5"> |
| | | <div class="elite-icon-tile"> |
| | | <IconifyIcon class="size-6" icon="lucide:file-text" /> |
| | | </div> |
| | | <div class="min-w-0 flex-1"> |
| | | <div class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">设å¤è¿è¡ç</div> |
| | | <div class="elite-label">课é¢ç³æ¥</div> |
| | | <div class="flex items-baseline gap-1"> |
| | | <CountTo class="text-2xl font-bold leading-tight text-violet-600 dark:text-violet-400" :decimals="1" :end-val="equipmentRate" :duration="1200" /> |
| | | <span class="text-xs text-gray-400">%</span> |
| | | <CountTo |
| | | class="elite-value" |
| | | :duration="1200" |
| | | :end-val="applicationCount" |
| | | /> |
| | | <span class="elite-unit">项</span> |
| | | </div> |
| | | <div class="mt-1 flex items-center gap-1 text-xs text-gray-400"> |
| | | <span class="text-red-400">åæº {{ summary.machineryStop }}</span> |
| | | <span class="mx-1">|</span> |
| | | <span class="text-amber-400">ç»´æ¤ {{ summary.machineryMaintenance }}</span> |
| | | </div> |
| | | <div class="elite-meta">课é¢ç®¡ç · ç³æ¥ä¹¦</div> |
| | | </div> |
| | | </div> |
| | | <div class="absolute inset-0 -z-0 bg-gradient-to-br from-violet-500/5 to-purple-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" /> |
| | | <div class="absolute -right-6 -top-6 size-24 rounded-full bg-violet-400/10 blur-xl transition-all duration-300 group-hover:scale-150" /> |
| | | </div> |
| | | </Col> |
| | | |
| | | <!-- è´¢å¡æ¥é --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="elite-card group cursor-pointer" |
| | | @click="emit('navigate', 'FinanceReimbursement')" |
| | | > |
| | | <div class="flex items-center gap-4 p-5"> |
| | | <div class="elite-icon-tile"> |
| | | <IconifyIcon class="size-6" icon="lucide:receipt" /> |
| | | </div> |
| | | <div class="min-w-0 flex-1"> |
| | | <div class="elite-label">è´¢å¡æ¥é</div> |
| | | <div class="flex items-baseline gap-1"> |
| | | <CountTo |
| | | class="elite-value" |
| | | :duration="1200" |
| | | :end-val="reimbursementCount" |
| | | /> |
| | | <span class="elite-unit">å</span> |
| | | </div> |
| | | <div class="elite-meta">è´¢å¡æ¥é · æ¥éå</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </Col> |
| | | |
| | | <!-- è¯¾é¢æå --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="elite-card group cursor-pointer" |
| | | @click="emit('navigate', 'ResearchGuideline')" |
| | | > |
| | | <div class="flex items-center gap-4 p-5"> |
| | | <div class="elite-icon-tile"> |
| | | <IconifyIcon class="size-6" icon="lucide:book-open" /> |
| | | </div> |
| | | <div class="min-w-0 flex-1"> |
| | | <div class="elite-label">è¯¾é¢æå</div> |
| | | <div class="flex items-baseline gap-1"> |
| | | <CountTo |
| | | class="elite-value" |
| | | :duration="1200" |
| | | :end-val="guidelineCount" |
| | | /> |
| | | <span class="elite-unit">份</span> |
| | | </div> |
| | | <div class="elite-meta">课é¢ç®¡ç · ç³æ¥æå</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </Col> |
| | | </Row> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .glass-tilt { |
| | | background: linear-gradient(135deg, rgba(255,255,255,0.85), rgba(255,255,255,0.55)); |
| | | backdrop-filter: blur(20px); |
| | | -webkit-backdrop-filter: blur(20px); |
| | | border: 1px solid rgba(255,255,255,0.6); |
| | | box-shadow: |
| | | 0 4px 24px rgba(0,0,0,0.04), |
| | | 0 1px 4px rgba(0,0,0,0.02), |
| | | inset 0 1px 0 rgba(255,255,255,0.8); |
| | | transform-style: preserve-3d; |
| | | /* ===== 妿¯å
¸é
å¡ç ===== */ |
| | | .elite-card { |
| | | background: #fdfcfa; |
| | | border: 1px solid rgba(212, 175, 55, 0.22); |
| | | border-radius: 14px; |
| | | box-shadow: 0 2px 14px rgba(15, 36, 64, 0.05); |
| | | transition: |
| | | border-color 0.3s ease, |
| | | box-shadow 0.3s ease; |
| | | } |
| | | |
| | | .dark .glass-tilt { |
| | | background: linear-gradient(135deg, rgba(30,30,55,0.75), rgba(20,20,40,0.5)); |
| | | border: 1px solid rgba(255,255,255,0.08); |
| | | box-shadow: |
| | | 0 4px 24px rgba(0,0,0,0.3), |
| | | 0 1px 4px rgba(0,0,0,0.2), |
| | | inset 0 1px 0 rgba(255,255,255,0.04); |
| | | .elite-card:hover { |
| | | border-color: rgba(212, 175, 55, 0.5); |
| | | box-shadow: 0 6px 20px rgba(15, 36, 64, 0.09); |
| | | } |
| | | |
| | | .dark .elite-card { |
| | | background: #101a2b; |
| | | border-color: rgba(212, 175, 55, 0.2); |
| | | box-shadow: 0 2px 14px rgba(0, 0, 0, 0.3); |
| | | } |
| | | |
| | | /* 徿 åºï¼è鿏å + éè²å¾æ */ |
| | | .elite-icon-tile { |
| | | display: flex; |
| | | width: 48px; |
| | | height: 48px; |
| | | flex-shrink: 0; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 12px; |
| | | color: #e3c567; |
| | | background: linear-gradient(135deg, #1e4571, #0f2440); |
| | | border: 1px solid rgba(212, 175, 55, 0.3); |
| | | box-shadow: 0 4px 12px rgba(15, 36, 64, 0.18); |
| | | transition: border-color 0.3s ease; |
| | | } |
| | | |
| | | /* æ¬ååªè®©éè²æè¾¹å¾®å¾®æäº®ï¼ä¸åç¼©æ¾ */ |
| | | .group:hover .elite-icon-tile { |
| | | border-color: rgba(212, 175, 55, 0.6); |
| | | } |
| | | |
| | | .elite-label { |
| | | margin-bottom: 4px; |
| | | font-size: 12px; |
| | | font-weight: 500; |
| | | letter-spacing: 1px; |
| | | color: #8a94a6; |
| | | } |
| | | |
| | | .elite-value { |
| | | font-size: 24px; |
| | | font-weight: 700; |
| | | line-height: 1.2; |
| | | color: #0f2440; |
| | | } |
| | | |
| | | .dark .elite-value { |
| | | color: #e3c567; |
| | | } |
| | | |
| | | .elite-unit { |
| | | font-size: 12px; |
| | | color: #8a94a6; |
| | | } |
| | | |
| | | .elite-meta { |
| | | margin-top: 4px; |
| | | font-size: 12px; |
| | | color: #8a94a6; |
| | | } |
| | | </style> |
| | |
| | | title: string; |
| | | icon: string; |
| | | route: string; |
| | | gradient: string; |
| | | shadow: string; |
| | | } |
| | | |
| | | /** å¿«æ·å
¥å£ï¼å¯¹åºç³»ç»å®é
èåï¼å®¡æ¹ä¸å¿ / 课é¢ç®¡ç / è´¢å¡æ¥é / éç¥å
¬åï¼ */ |
| | | const actions: QuickAction[] = [ |
| | | { title: 'ç产工å', icon: 'lucide:file-text', route: 'MesProWorkOrder', gradient: 'from-blue-500 to-cyan-500', shadow: 'shadow-blue-500/15' }, |
| | | { title: 'ç产æ¥å·¥', icon: 'lucide:check-square', route: 'MesProFeedback', gradient: 'from-emerald-500 to-green-500', shadow: 'shadow-emerald-500/15' }, |
| | | { title: 'è´¨æ£ç®¡ç', icon: 'lucide:clipboard-check', route: 'MesQcRecord', gradient: 'from-orange-500 to-amber-500', shadow: 'shadow-orange-500/15' }, |
| | | { title: '设å¤ç¹æ£', icon: 'lucide:settings', route: 'MesDvMachinery', gradient: 'from-violet-500 to-purple-500', shadow: 'shadow-violet-500/15' }, |
| | | { title: 'ç产æç¨', icon: 'lucide:calendar-range', route: 'MesProScheduling', gradient: 'from-cyan-500 to-teal-500', shadow: 'shadow-cyan-500/15' }, |
| | | { title: 'ç©æåºå', icon: 'lucide:package-search', route: 'WlsMaterialStock', gradient: 'from-indigo-500 to-blue-500', shadow: 'shadow-indigo-500/15' }, |
| | | { title: 'å·¥èºè·¯çº¿', icon: 'lucide:route', route: 'MesPdRouting', gradient: 'from-teal-500 to-emerald-500', shadow: 'shadow-teal-500/15' }, |
| | | { title: 'æ°æ®æ¥è¡¨', icon: 'lucide:bar-chart-big', route: 'MesHomeIndex', gradient: 'from-pink-500 to-rose-500', shadow: 'shadow-pink-500/15' }, |
| | | { title: 'åèµ·æµç¨', icon: 'lucide:circle-plus', route: 'BpmProcessInstanceCreate' }, |
| | | { title: 'å¾
åä»»å¡', icon: 'lucide:list-todo', route: 'BpmTaskTodo' }, |
| | | { title: 'å·²åä»»å¡', icon: 'lucide:circle-check-big', route: 'BpmTaskDone' }, |
| | | { title: 'æçæµç¨', icon: 'lucide:git-branch', route: 'BpmProcessInstance' }, |
| | | { title: '课é¢ç³æ¥', icon: 'lucide:file-text', route: 'ResearchApplication' }, |
| | | { title: 'è¯¾é¢æå', icon: 'lucide:book-open', route: 'ResearchGuideline' }, |
| | | { title: 'è´¢å¡æ¥é', icon: 'lucide:receipt', route: 'FinanceReimbursement' }, |
| | | { title: 'éç¥å
Œ', icon: 'lucide:bell', route: 'BpmOaNotice' }, |
| | | ]; |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="glass-card rounded-2xl p-5"> |
| | | <h3 class="mb-4 text-sm font-semibold text-gray-800 dark:text-gray-100">å¿«æ·æä½</h3> |
| | | <div class="grid grid-cols-4 gap-3 sm:grid-cols-8"> |
| | | <div class="elite-card p-5"> |
| | | <div class="elite-head"> |
| | | <h3 class="elite-title"> |
| | | <span class="elite-bar"></span> |
| | | å¿«æ·å
¥å£ |
| | | </h3> |
| | | </div> |
| | | <div class="grid grid-cols-4 gap-3"> |
| | | <div |
| | | v-for="action in actions" |
| | | :key="action.route" |
| | | class="group flex cursor-pointer flex-col items-center gap-2 rounded-xl p-3 transition-all duration-300 hover:-translate-y-1" |
| | | class="elite-action group flex cursor-pointer flex-col items-center gap-2 rounded-xl p-3 transition-colors duration-300" |
| | | @click="emit('navigate', action.route)" |
| | | > |
| | | <div |
| | | class="flex size-10 items-center justify-center rounded-xl bg-gradient-to-br transition-shadow duration-300 group-hover:shadow-lg" |
| | | :class="[action.gradient, action.shadow]" |
| | | class="elite-icon flex size-10 items-center justify-center rounded-xl transition-shadow duration-300" |
| | | > |
| | | <IconifyIcon :icon="action.icon" class="size-5 text-white" /> |
| | | <IconifyIcon :icon="action.icon" class="size-5 text-[#e3c567]" /> |
| | | </div> |
| | | <span class="text-xs font-medium text-gray-600 dark:text-gray-400">{{ action.title }}</span> |
| | | <span class="elite-action-label">{{ action.title }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .glass-card { |
| | | background: linear-gradient(135deg, rgba(255,255,255,0.85), rgba(255,255,255,0.55)); |
| | | backdrop-filter: blur(20px); |
| | | -webkit-backdrop-filter: blur(20px); |
| | | border: 1px solid rgba(255,255,255,0.6); |
| | | box-shadow: |
| | | 0 4px 24px rgba(0,0,0,0.04), |
| | | 0 1px 4px rgba(0,0,0,0.02), |
| | | inset 0 1px 0 rgba(255,255,255,0.8); |
| | | .elite-card { |
| | | background: #fdfcfa; |
| | | border: 1px solid rgba(212, 175, 55, 0.22); |
| | | border-radius: 14px; |
| | | box-shadow: 0 2px 14px rgba(15, 36, 64, 0.05); |
| | | transition: |
| | | border-color 0.3s ease, |
| | | box-shadow 0.3s ease; |
| | | } |
| | | |
| | | .dark .glass-card { |
| | | background: linear-gradient(135deg, rgba(30,30,55,0.75), rgba(20,20,40,0.5)); |
| | | border: 1px solid rgba(255,255,255,0.08); |
| | | .elite-card:hover { |
| | | border-color: rgba(212, 175, 55, 0.5); |
| | | box-shadow: 0 6px 20px rgba(15, 36, 64, 0.09); |
| | | } |
| | | |
| | | .dark .elite-card { |
| | | background: #101a2b; |
| | | border-color: rgba(212, 175, 55, 0.2); |
| | | box-shadow: 0 2px 14px rgba(0, 0, 0, 0.3); |
| | | } |
| | | |
| | | .elite-head { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .elite-title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | margin: 0; |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | letter-spacing: 1px; |
| | | color: #0f2440; |
| | | } |
| | | |
| | | .dark .elite-title { |
| | | color: #f0ead8; |
| | | } |
| | | |
| | | .elite-bar { |
| | | display: inline-block; |
| | | width: 3px; |
| | | height: 15px; |
| | | border-radius: 2px; |
| | | background: linear-gradient(180deg, #e3c567, #b8901f); |
| | | } |
| | | |
| | | /* 徿 åºï¼è鿏å + éè²å¾æ */ |
| | | .elite-icon { |
| | | color: #e3c567; |
| | | background: linear-gradient(135deg, #1e4571, #0f2440); |
| | | border: 1px solid rgba(212, 175, 55, 0.3); |
| | | box-shadow: 0 4px 12px rgba(15, 36, 64, 0.18); |
| | | } |
| | | |
| | | /* æ¬ååªæäº®éè²æè¾¹ï¼ä¸åä½ç§» */ |
| | | .elite-action:hover .elite-icon { |
| | | box-shadow: |
| | | 0 4px 24px rgba(0,0,0,0.3), |
| | | 0 1px 4px rgba(0,0,0,0.2), |
| | | inset 0 1px 0 rgba(255,255,255,0.04); |
| | | 0 0 0 1px rgba(212, 175, 55, 0.7), |
| | | 0 4px 14px rgba(15, 36, 64, 0.22); |
| | | } |
| | | |
| | | .elite-action-label { |
| | | font-size: 12px; |
| | | font-weight: 500; |
| | | letter-spacing: 0.5px; |
| | | color: #8a94a6; |
| | | } |
| | | |
| | | .elite-action:hover .elite-action-label { |
| | | color: #b8901f; |
| | | } |
| | | |
| | | .dark .elite-action:hover .elite-action-label { |
| | | color: #e3c567; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import { onMounted, ref } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { getApplicationPage } from '#/api/research/application'; |
| | | |
| | | defineOptions({ name: 'DashboardRecentApplications' }); |
| | | |
| | | const emit = defineEmits<{ |
| | | navigate: [name: string]; |
| | | }>(); |
| | | |
| | | interface Row { |
| | | id?: number; |
| | | no?: string; |
| | | title: string; |
| | | applicantName: string; |
| | | status?: number; |
| | | createTime?: string; |
| | | } |
| | | |
| | | /** ç¶æç â å±ç¤ºææ¡ä¸é
è²ï¼ä¸ research_application_status åå
¸ä¿æä¸è´ */ |
| | | const STATUS_MAP: Record<number, { text: string; tone: string }> = { |
| | | 0: { text: 'è稿', tone: 'gray' }, |
| | | 15: { text: '审æ¹ä¸', tone: 'gold' }, |
| | | 20: { text: 'å·²ç«é¡¹', tone: 'green' }, |
| | | 35: { text: '已驳å', tone: 'red' }, |
| | | }; |
| | | |
| | | function statusOf(code?: number) { |
| | | if (code === undefined || code === null) { |
| | | return { text: 'â', tone: 'gray' }; |
| | | } |
| | | return STATUS_MAP[code] ?? { text: String(code), tone: 'gray' }; |
| | | } |
| | | |
| | | const loading = ref(true); |
| | | const rows = ref<Row[]>([]); |
| | | |
| | | async function load() { |
| | | loading.value = true; |
| | | try { |
| | | const result = await getApplicationPage({ pageNo: 1, pageSize: 6 }); |
| | | rows.value = (result.list ?? []) as Row[]; |
| | | } catch { |
| | | rows.value = []; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | onMounted(load); |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="elite-section"> |
| | | <div class="elite-head"> |
| | | <h3 class="elite-title"> |
| | | <span class="elite-bar"></span> |
| | | æè¿è¯¾é¢ç³æ¥ |
| | | </h3> |
| | | <span class="elite-more" @click="emit('navigate', 'ResearchApplication')"> |
| | | å
¨é¨ |
| | | <IconifyIcon class="size-3.5" icon="lucide:chevron-right" /> |
| | | </span> |
| | | </div> |
| | | |
| | | <div v-if="loading" class="elite-empty">å è½½ä¸â¦</div> |
| | | |
| | | <div v-else-if="rows.length === 0" class="elite-empty"> |
| | | <IconifyIcon class="mb-2 size-8 opacity-40" icon="lucide:file-text" /> |
| | | <div>ææ è¯¾é¢ç³æ¥è®°å½</div> |
| | | </div> |
| | | |
| | | <table v-else class="elite-table"> |
| | | <thead> |
| | | <tr> |
| | | <th class="col-title">课é¢åç§°</th> |
| | | <th class="col-applicant">ç³æ¥äºº</th> |
| | | <th class="col-status">ç¶æ</th> |
| | | <th class="col-time">ç³æ¥æ¶é´</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <tr |
| | | v-for="row in rows" |
| | | :key="row.id ?? row.no" |
| | | @click="emit('navigate', 'ResearchApplication')" |
| | | > |
| | | <td class="col-title"> |
| | | <span class="elite-ellipsis">{{ row.title }}</span> |
| | | </td> |
| | | <td class="col-applicant">{{ row.applicantName || 'â' }}</td> |
| | | <td class="col-status"> |
| | | <span class="elite-chip" :class="`tone-${statusOf(row.status).tone}`"> |
| | | {{ statusOf(row.status).text }} |
| | | </span> |
| | | </td> |
| | | <td class="col-time">{{ row.createTime || 'â' }}</td> |
| | | </tr> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .elite-section { |
| | | border-radius: 14px; |
| | | padding: 20px; |
| | | background: #fdfcfa; |
| | | border: 1px solid rgba(212, 175, 55, 0.22); |
| | | box-shadow: 0 2px 14px rgba(15, 36, 64, 0.05); |
| | | } |
| | | |
| | | .dark .elite-section { |
| | | background: #101a2b; |
| | | border-color: rgba(212, 175, 55, 0.2); |
| | | box-shadow: 0 2px 14px rgba(0, 0, 0, 0.3); |
| | | } |
| | | |
| | | .elite-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-bottom: 14px; |
| | | } |
| | | |
| | | .elite-title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | margin: 0; |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | letter-spacing: 1px; |
| | | color: #0f2440; |
| | | } |
| | | |
| | | .dark .elite-title { |
| | | color: #f0ead8; |
| | | } |
| | | |
| | | .elite-bar { |
| | | display: inline-block; |
| | | width: 3px; |
| | | height: 15px; |
| | | border-radius: 2px; |
| | | background: linear-gradient(180deg, #e3c567, #b8901f); |
| | | } |
| | | |
| | | .elite-more { |
| | | display: inline-flex; |
| | | cursor: pointer; |
| | | align-items: center; |
| | | gap: 2px; |
| | | font-size: 12px; |
| | | color: #b8901f; |
| | | transition: color 0.2s ease; |
| | | } |
| | | |
| | | .elite-more:hover { |
| | | color: #8a6a12; |
| | | } |
| | | |
| | | .elite-empty { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | padding: 34px 0; |
| | | font-size: 13px; |
| | | color: #8a94a6; |
| | | } |
| | | |
| | | .elite-table { |
| | | width: 100%; |
| | | border-collapse: collapse; |
| | | table-layout: fixed; |
| | | } |
| | | |
| | | .elite-table th { |
| | | padding: 0 8px 10px; |
| | | font-size: 12px; |
| | | font-weight: 500; |
| | | letter-spacing: 1px; |
| | | color: #8a94a6; |
| | | text-align: left; |
| | | border-bottom: 1px solid rgba(15, 36, 64, 0.08); |
| | | } |
| | | |
| | | .elite-table td { |
| | | padding: 12px 8px; |
| | | font-size: 13.5px; |
| | | color: #0f2440; |
| | | border-bottom: 1px solid rgba(15, 36, 64, 0.05); |
| | | } |
| | | |
| | | .dark .elite-table td { |
| | | color: #e6e9ef; |
| | | } |
| | | |
| | | .elite-table tbody tr { |
| | | cursor: pointer; |
| | | transition: background-color 0.2s ease; |
| | | } |
| | | |
| | | .elite-table tbody tr:hover { |
| | | background: rgba(212, 175, 55, 0.07); |
| | | } |
| | | |
| | | .elite-table tbody tr:last-child td { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .col-title { |
| | | width: 44%; |
| | | } |
| | | |
| | | .col-applicant { |
| | | width: 18%; |
| | | } |
| | | |
| | | .col-status { |
| | | width: 16%; |
| | | } |
| | | |
| | | .col-time { |
| | | width: 22%; |
| | | color: #8a94a6; |
| | | } |
| | | |
| | | .elite-ellipsis { |
| | | display: block; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | /* ç¶ææ ç¾ï¼é/绿/红/ç°åæ¡£ */ |
| | | .elite-chip { |
| | | display: inline-block; |
| | | padding: 2px 10px; |
| | | font-size: 12px; |
| | | border-radius: 999px; |
| | | border: 1px solid transparent; |
| | | } |
| | | |
| | | .tone-gold { |
| | | color: #b8901f; |
| | | background: rgba(212, 175, 55, 0.1); |
| | | border-color: rgba(212, 175, 55, 0.35); |
| | | } |
| | | |
| | | .tone-green { |
| | | color: #2f6b4f; |
| | | background: rgba(47, 107, 79, 0.1); |
| | | border-color: rgba(47, 107, 79, 0.3); |
| | | } |
| | | |
| | | .tone-red { |
| | | color: #b4453c; |
| | | background: rgba(180, 69, 60, 0.1); |
| | | border-color: rgba(180, 69, 60, 0.3); |
| | | } |
| | | |
| | | .tone-gray { |
| | | color: #8a94a6; |
| | | background: rgba(138, 148, 166, 0.1); |
| | | border-color: rgba(138, 148, 166, 0.28); |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import { onMounted, ref } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { getTaskTodoPage } from '#/api/bpm/task'; |
| | | |
| | | defineOptions({ name: 'DashboardTodoPanel' }); |
| | | |
| | | const emit = defineEmits<{ |
| | | navigate: [name: string]; |
| | | }>(); |
| | | |
| | | interface TodoItem { |
| | | id: string; |
| | | name: string; |
| | | createTime?: number; |
| | | } |
| | | |
| | | const loading = ref(true); |
| | | const items = ref<TodoItem[]>([]); |
| | | |
| | | /** createTime ä¸ºæ¯«ç§æ¶é´æ³ï¼æ ¼å¼åææè¯»ææ¬ */ |
| | | function formatTime(ts?: number) { |
| | | if (!ts) return '-'; |
| | | const d = new Date(ts); |
| | | const p = (n: number) => String(n).padStart(2, '0'); |
| | | return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`; |
| | | } |
| | | |
| | | async function load() { |
| | | loading.value = true; |
| | | try { |
| | | const result = await getTaskTodoPage({ pageNo: 1, pageSize: 6 }); |
| | | items.value = (result.list ?? []).map((t: any) => ({ |
| | | id: String(t.id), |
| | | name: t.name ?? 'æªå½åä»»å¡', |
| | | createTime: t.createTime, |
| | | })); |
| | | } catch { |
| | | items.value = []; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | onMounted(load); |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="elite-section h-full"> |
| | | <div class="elite-head"> |
| | | <h3 class="elite-title"> |
| | | <span class="elite-bar"></span> |
| | | æçå¾
å |
| | | </h3> |
| | | <span class="elite-more" @click="emit('navigate', 'BpmTaskTodo')"> |
| | | å
¨é¨ |
| | | <IconifyIcon class="size-3.5" icon="lucide:chevron-right" /> |
| | | </span> |
| | | </div> |
| | | |
| | | <!-- éª¨æ¶ --> |
| | | <div v-if="loading" class="elite-empty">å è½½ä¸â¦</div> |
| | | |
| | | <!-- ç©ºç¶æ --> |
| | | <div v-else-if="items.length === 0" class="elite-empty"> |
| | | <IconifyIcon class="mb-2 size-8 opacity-40" icon="lucide:inbox" /> |
| | | <div>ææ å¾
åä»»å¡</div> |
| | | </div> |
| | | |
| | | <!-- å表 --> |
| | | <ul v-else class="elite-list"> |
| | | <li |
| | | v-for="item in items" |
| | | :key="item.id" |
| | | class="elite-row" |
| | | @click="emit('navigate', 'BpmTaskTodo')" |
| | | > |
| | | <span class="elite-dot"></span> |
| | | <span class="elite-row-name">{{ item.name }}</span> |
| | | <span class="elite-row-time">{{ formatTime(item.createTime) }}</span> |
| | | </li> |
| | | </ul> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .elite-section { |
| | | border-radius: 14px; |
| | | padding: 20px; |
| | | background: #fdfcfa; |
| | | border: 1px solid rgba(212, 175, 55, 0.22); |
| | | box-shadow: 0 2px 14px rgba(15, 36, 64, 0.05); |
| | | } |
| | | |
| | | .dark .elite-section { |
| | | background: #101a2b; |
| | | border-color: rgba(212, 175, 55, 0.2); |
| | | box-shadow: 0 2px 14px rgba(0, 0, 0, 0.3); |
| | | } |
| | | |
| | | .elite-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-bottom: 14px; |
| | | } |
| | | |
| | | .elite-title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | margin: 0; |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | letter-spacing: 1px; |
| | | color: #0f2440; |
| | | } |
| | | |
| | | .dark .elite-title { |
| | | color: #f0ead8; |
| | | } |
| | | |
| | | .elite-bar { |
| | | display: inline-block; |
| | | width: 3px; |
| | | height: 15px; |
| | | border-radius: 2px; |
| | | background: linear-gradient(180deg, #e3c567, #b8901f); |
| | | } |
| | | |
| | | .elite-more { |
| | | display: inline-flex; |
| | | cursor: pointer; |
| | | align-items: center; |
| | | gap: 2px; |
| | | font-size: 12px; |
| | | color: #b8901f; |
| | | transition: color 0.2s ease; |
| | | } |
| | | |
| | | .elite-more:hover { |
| | | color: #8a6a12; |
| | | } |
| | | |
| | | .elite-empty { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | padding: 34px 0; |
| | | font-size: 13px; |
| | | color: #8a94a6; |
| | | } |
| | | |
| | | .elite-list { |
| | | margin: 0; |
| | | padding: 0; |
| | | list-style: none; |
| | | } |
| | | |
| | | .elite-row { |
| | | display: flex; |
| | | cursor: pointer; |
| | | align-items: center; |
| | | gap: 10px; |
| | | padding: 11px 10px; |
| | | border-radius: 8px; |
| | | border-bottom: 1px solid rgba(15, 36, 64, 0.05); |
| | | transition: background-color 0.2s ease; |
| | | } |
| | | |
| | | .elite-row:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .elite-row:hover { |
| | | background: rgba(212, 175, 55, 0.07); |
| | | } |
| | | |
| | | .elite-dot { |
| | | width: 6px; |
| | | height: 6px; |
| | | flex-shrink: 0; |
| | | border-radius: 999px; |
| | | background: #d4af37; |
| | | } |
| | | |
| | | .elite-row-name { |
| | | overflow: hidden; |
| | | flex: 1; |
| | | font-size: 13.5px; |
| | | color: #0f2440; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .dark .elite-row-name { |
| | | color: #e6e9ef; |
| | | } |
| | | |
| | | .elite-row-time { |
| | | flex-shrink: 0; |
| | | font-size: 12px; |
| | | color: #8a94a6; |
| | | } |
| | | </style> |
| | |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await archiveReimbursement(row.id!); |
| | | message.success('彿¡£æå'); |
| | | const archiveNo = await archiveReimbursement(row.id!); |
| | | message.success(`已彿¡£å¹¶çææ¡£æ¡ ${archiveNo}`); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | |
| | | /** ç»è´¹æ¨ä»ç¶æï¼å¯¹åºåå
¸ research_fund_statusï¼ */ |
| | | export const FundStatus = { |
| | | PENDING: 0, // å¾
æ¨ä» |
| | | APPROVING: 15, // æ¥é审æ¹ä¸ |
| | | PAID: 20, // å·²æ¨ä» |
| | | REJECTED: 30, // 已驳å |
| | | } as const; |
| | | |
| | | /** 课é¢ç³æ¥ä¹¦è¡¨åé
ç½® */ |
| | |
| | | { |
| | | field: 'status', |
| | | title: 'æ¨ä»ç¶æ', |
| | | minWidth: 100, |
| | | minWidth: 110, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.RESEARCH_FUND_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'reimbursementNo', |
| | | title: 'è´¢å¡æ¥éåå·', |
| | | minWidth: 160, |
| | | formatter: dashIfEmpty, |
| | | }, |
| | | { |
| | | field: 'rejectReason', |
| | | title: '驳ååå ', |
| | | minWidth: 200, |
| | | formatter: dashIfEmpty, |
| | | }, |
| | | { |
| | | field: 'hasAttachment', |
| | | title: 'æ¨ä»åè¯', |
| | | minWidth: 100, |
| | |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | archiveApplication, |
| | | deleteApplication, |
| | | getApplicationApproveProcessList, |
| | | getApplicationPage, |
| | |
| | | name: 'BpmProcessInstanceDetail', |
| | | query: { id: row.processInstanceId }, |
| | | }); |
| | | } |
| | | |
| | | /** 彿¡£å°æ¡£æ¡ç®¡çï¼ä»
å·²ç«é¡¹å¯å½æ¡£ï¼ */ |
| | | async function handleArchive(row: ResearchApplicationApi.Application) { |
| | | const hideLoading = message.loading({ content: '彿¡£ä¸...', duration: 0 }); |
| | | try { |
| | | const archiveNo = await archiveApplication(row.id!); |
| | | message.success(`彿¡£æåï¼å·²çææ¡£æ¡ ${archiveNo}`); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | const checkedIds = ref<number[]>([]); |
| | |
| | | onClick: handlePublishNotice.bind(null, row), |
| | | }, |
| | | { |
| | | label: '彿¡£', |
| | | type: 'link', |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['research:application:archive'], |
| | | ifShow: () => |
| | | row.status === ApplicationStatus.APPROVED && |
| | | !row.archiveDocumentId, |
| | | popConfirm: { |
| | | title: `ç¡®è®¤å½æ¡£è¯¾é¢ ${row.researchNo || row.no} åï¼å½æ¡£åå°å¨æ¡£æ¡ç®¡ççæå¯¹åºæ¡£æ¡`, |
| | | confirm: handleArchive.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: 'æ¥ç审æ¹', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Alert, message } from 'ant-design-vue'; |
| | |
| | | const isEdit = ref(false); |
| | | /** ç³è¯·ç»è´¹åå»å·²ç»è®°æ¨ä»éé¢åçå¯ç¨ä½é¢ */ |
| | | const remainder = ref<number>(); |
| | | /** ç¼è¾ã已驳åãè®°å½æ¶å±ç¤ºçè´¢å¡é©³ååå */ |
| | | const rejectReason = ref(''); |
| | | |
| | | const alertMessage = computed(() => { |
| | | if (remainder.value === undefined) { |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'æ¨ä»ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.RESEARCH_FUND_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©æ¨ä»ç¶æ', |
| | | }, |
| | | rules: 'selectRequired', |
| | | help: 'éæ©ãå·²æ¨ä»ãæ¶å¿
é¡»ä¸ä¼ æ¨ä»åè¯éä»¶', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | |
| | | multiple: true, |
| | | showDescription: true, |
| | | }, |
| | | rules: 'required', |
| | | help: 'å¿
å¡«ï¼åè¯å°éèªå¨çæçè´¢å¡æ¥éåä¸èµ·é交财å¡å®¡æ¹', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ], |
| | |
| | | } |
| | | const values = |
| | | (await formApi.getValues()) as ResearchFundRecordApi.FundRecord; |
| | | // ãå·²æ¨ä»ãå¿
é¡»æåè¯ï¼å
å¨å端æ¦ä¸éï¼é¿å
ç¨æ·ç½ç䏿¬¡è¯·æ± |
| | | if ( |
| | | values.status === FundStatus.PAID && |
| | | (!values.blobIds || values.blobIds.length === 0) |
| | | ) { |
| | | message.warning('æ¨ä»ç¶æä¸ºãå·²æ¨ä»ãæ¶ï¼è¯·å
ä¸ä¼ æ¨ä»åè¯éä»¶'); |
| | | // åè¯å¿
å¡«ï¼å
å¨å端æ¦ä¸éï¼é¿å
ç¨æ·ç½ç䏿¬¡è¯·æ± |
| | | if (!values.blobIds || values.blobIds.length === 0) { |
| | | message.warning('请å
ä¸ä¼ æ¬æ¬¡æ¨ä»åè¯ï¼åè¯å°éè´¢å¡æ¥éåé交财å¡å®¡æ¹'); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | |
| | | amount: values.amount, |
| | | payDate: values.payDate, |
| | | payee: values.payee, |
| | | status: values.status, |
| | | remark: values.remark, |
| | | blobIds: values.blobIds ?? [], |
| | | blobIds: values.blobIds, |
| | | } as ResearchFundRecordApi.FundRecord; |
| | | try { |
| | | await (data.id ? updateFundRecord(data) : createFundRecord(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success('ä¿åæå'); |
| | | message.success( |
| | | data.id ? '已鿰åèµ·è´¢å¡æ¥é审æ¹' : 'ç»è®°æåï¼å·²èªå¨æäº¤è´¢å¡æ¥é审æ¹', |
| | | ); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | if (!isOpen) { |
| | | isEdit.value = false; |
| | | remainder.value = undefined; |
| | | rejectReason.value = ''; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ |
| | |
| | | }>(); |
| | | isEdit.value = !!data.record; |
| | | remainder.value = data.remainder; |
| | | rejectReason.value = |
| | | data.record?.status === FundStatus.REJECTED |
| | | ? data.record.rejectReason || 'è´¢å¡æªå¡«å驳ååå ' |
| | | : ''; |
| | | await formApi.resetForm(); |
| | | if (data.record) { |
| | | const blobIds = (data.record.attachmentList ?? []).map((item) => ({ |
| | |
| | | await formApi.setValues( |
| | | { |
| | | applicationId: data.applicationId, |
| | | status: FundStatus.PENDING, |
| | | }, |
| | | false, |
| | | ); |
| | |
| | | |
| | | <template> |
| | | <Modal |
| | | :title="isEdit ? 'ç¼è¾æ¨æ¬¾è®°å½' : 'ç»è®°æ¨æ¬¾è®°å½'" |
| | | :title="isEdit ? 'ä¿®æ¹å¹¶éæ°åèµ·æ¥é' : 'ç»è®°æ¨æ¬¾ï¼ç»è®°åèªå¨æäº¤è´¢å¡æ¥é审æ¹ï¼'" |
| | | class="w-1/2" |
| | | :close-on-click-modal="false" |
| | | > |
| | | <Alert |
| | | v-if="rejectReason" |
| | | class="mx-3 mb-3" |
| | | type="error" |
| | | show-icon |
| | | :message="`è´¢å¡æ¥é已驳åï¼${rejectReason}`" |
| | | /> |
| | | <Alert |
| | | v-if="alertMessage" |
| | | class="mx-3 mb-3" |
| | | type="info" |
| | |
| | | () => (Number(application.value?.budgetAmount) || 0) - registeredAmount.value, |
| | | ); |
| | | |
| | | /** åç¶æè®°å½æ°éï¼ç¨äºé¡¶é¨æç¤ºï¼0 æ¡æ¶ä¸æç¤ºï¼ */ |
| | | const statusCount = (status: number) => |
| | | records.value.filter((item) => item.status === status).length; |
| | | |
| | | const [FundFormModal, fundFormModalApi] = useVbenModal({ |
| | | connectedComponent: FundForm, |
| | | destroyOnClose: true, |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <Alert |
| | | v-if="statusCount(FundStatus.APPROVING) > 0" |
| | | type="info" |
| | | show-icon |
| | | :message="`æ ${statusCount(FundStatus.APPROVING)} æ¡æ¨ä»è®°å½æ£å¨æ¥é审æ¹ä¸ï¼æä¸å¯ä¿®æ¹ï¼è´¢å¡å®¡æ¹ç»æå°èªå¨ååæ¬è®°å½`" |
| | | /> |
| | | <Alert |
| | | v-if="statusCount(FundStatus.PAID) > 0" |
| | | type="success" |
| | | show-icon |
| | | :message="`æ ${statusCount(FundStatus.PAID)} æ¡æ¨ä»è®°å½è´¢å¡å·²éè¿ï¼æ¨æ¬¾å®æ`" |
| | | /> |
| | | <Alert |
| | | v-if="statusCount(FundStatus.REJECTED) > 0" |
| | | type="error" |
| | | show-icon |
| | | :message="`æ ${statusCount(FundStatus.REJECTED)} æ¡æ¨ä»è®°å½è´¢å¡å·²é©³åï¼è¯·ä¿®æ¹é颿åè¯åç¹ãéæ°åèµ·æ¥éã`" |
| | | /> |
| | | |
| | | <div v-if="editable"> |
| | | <TableAction |
| | | :actions="[ |
| | |
| | | <Grid> |
| | | <template #hasAttachment="{ row }"> |
| | | <Tag :color="row.hasAttachment ? 'success' : 'warning'"> |
| | | {{ |
| | | row.hasAttachment |
| | | ? 'å·²ä¸ä¼ ' |
| | | : row.status === FundStatus.PAID |
| | | ? '缺失' |
| | | : 'ä¸éè¦' |
| | | }} |
| | | {{ row.hasAttachment ? 'å·²ä¸ä¼ ' : '缺失' }} |
| | | </Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'éæ°åèµ·æ¥é', |
| | | type: 'link', |
| | | icon: ACTION_ICON.REFRESH, |
| | | auth: ['research:fund:update'], |
| | | ifShow: () => row.status === FundStatus.REJECTED, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'å é¤', |
| | | type: 'link', |
| | | danger: true, |
| | |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { deleteMember, getMemberPage } from '#/api/ums/member'; |
| | | import { |
| | | archiveMember, |
| | | deleteMember, |
| | | getMemberPage, |
| | | } from '#/api/ums/member'; |
| | | |
| | | import { MemberType, useGridColumns, useGridFormSchema } from './data'; |
| | | import { |
| | | MemberStatus, |
| | | MemberType, |
| | | useGridColumns, |
| | | useGridFormSchema, |
| | | } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | /** ä¼åå表 */ |
| | |
| | | const checkedIds = ref<number[]>([]); |
| | | function handleRowCheckboxChange({ records }: { records: UmsMemberApi.Member[] }) { |
| | | checkedIds.value = records.map((item) => item.id!); |
| | | } |
| | | |
| | | /** 彿¡£å°æ¡£æ¡ç®¡çï¼ä»
æ£å¸¸ç¶æä¼åå¯å½æ¡£ï¼ */ |
| | | async function handleArchive(row: UmsMemberApi.Member) { |
| | | const hideLoading = message.loading({ content: '彿¡£ä¸...', duration: 0 }); |
| | | try { |
| | | const archiveNo = await archiveMember(row.id!); |
| | | message.success(`彿¡£æåï¼å·²çææ¡£æ¡ ${archiveNo}`); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** ç级åå
¸æ å°ï¼æ ¹æ®ä¼åç±»åæ¾ç¤ºå¯¹åºçç级åç§° */ |
| | |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: '彿¡£', |
| | | type: 'link', |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['ums:member:archive'], |
| | | ifShow: () => |
| | | row.status === MemberStatus.NORMAL && !row.archiveDocumentId, |
| | | popConfirm: { |
| | | title: `ç¡®è®¤å½æ¡£ä¼å ${row.name} åï¼å½æ¡£åå°å¨æ¡£æ¡ç®¡ççæå¯¹åºæ¡£æ¡`, |
| | | confirm: handleArchive.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: 'å é¤', |
| | | type: 'link', |
| | | danger: true, |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { WxCpActivityApi } from '#/api/wxcp/activity'; |
| | | |
| | | import { |
| | | ACTIVITY_STATUS_OPTIONS, |
| | | PUSH_SCOPE_OPTIONS, |
| | | PUSH_STATUS_OPTIONS, |
| | | SIGNUP_STATUS_OPTIONS, |
| | | } from '../data'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type ActivityFormType = 'create' | 'update' | 'detail'; |
| | | |
| | | const DATETIME_PROPS = { |
| | | showTime: true, |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | } as const; |
| | | |
| | | /** æ´»å¨è¡¨å */ |
| | | export function useActivityFormSchema( |
| | | formType: ActivityFormType, |
| | | ): VbenFormSchema[] { |
| | | const readonly = formType === 'detail'; |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'title', |
| | | label: 'æ´»å¨æ é¢', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ´»å¨æ é¢', |
| | | disabled: readonly, |
| | | }, |
| | | rules: 'required', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'location', |
| | | label: 'æ´»å¨å°ç¹', |
| | | component: 'Input', |
| | | componentProps: { placeholder: 'å¦ï¼å
¬å¸å¤§ä¼è®®å®¤', disabled: readonly }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'startTime', |
| | | label: 'æ´»å¨å¼å§', |
| | | component: 'DatePicker', |
| | | componentProps: { ...DATETIME_PROPS, disabled: readonly }, |
| | | }, |
| | | { |
| | | fieldName: 'endTime', |
| | | label: 'æ´»å¨ç»æ', |
| | | component: 'DatePicker', |
| | | componentProps: { ...DATETIME_PROPS, disabled: readonly }, |
| | | }, |
| | | { |
| | | fieldName: 'signupStartTime', |
| | | label: 'æ¥åå¼å§', |
| | | component: 'DatePicker', |
| | | componentProps: { ...DATETIME_PROPS, disabled: readonly }, |
| | | }, |
| | | { |
| | | fieldName: 'signupEndTime', |
| | | label: 'æ¥åæªæ¢', |
| | | component: 'DatePicker', |
| | | componentProps: { ...DATETIME_PROPS, disabled: readonly }, |
| | | }, |
| | | { |
| | | fieldName: 'signupLimit', |
| | | label: 'æ¥åä¸é', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | placeholder: 'ä¸é请填 0', |
| | | min: 0, |
| | | class: 'w-full', |
| | | disabled: readonly, |
| | | }, |
| | | rules: 'required', |
| | | help: 'å¡« 0 表示ä¸éåé¢', |
| | | }, |
| | | { |
| | | fieldName: 'coverUrl', |
| | | label: 'å°é¢å¾', |
| | | component: 'Input', |
| | | componentProps: { placeholder: 'å°é¢å¾å°åï¼éå¡«ï¼', disabled: readonly }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'content', |
| | | label: 'æ´»å¨è¯¦æ
', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | rows: 4, |
| | | placeholder: 'æ´»å¨ä»ç»ã注æäºé¡¹çï¼æ¥å页æçº¯ææ¬å±ç¤ºï¼', |
| | | disabled: readonly, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { rows: 2, placeholder: 'å
é¨å¤æ³¨ï¼ä¸å±ç¤ºç»æå', disabled: readonly }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** æ´»å¨æç´¢è¡¨å */ |
| | | export function useActivitySearchFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'title', |
| | | label: 'æ´»å¨æ é¢', |
| | | component: 'Input', |
| | | componentProps: { allowClear: true, placeholder: '请è¾å
¥æ´»å¨æ é¢' }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: ACTIVITY_STATUS_OPTIONS, |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'pushStatus', |
| | | label: 'æ¨éç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: PUSH_STATUS_OPTIONS, |
| | | placeholder: 'è¯·éæ©æ¨éç¶æ', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** æ´»å¨åè¡¨åæ®µ */ |
| | | export function useActivityGridColumns(): VxeTableGridOptions<WxCpActivityApi.Activity>['columns'] { |
| | | return [ |
| | | { |
| | | field: 'title', |
| | | title: 'æ´»å¨æ é¢', |
| | | minWidth: 200, |
| | | slots: { default: 'title' }, |
| | | }, |
| | | { field: 'location', title: 'æ´»å¨å°ç¹', minWidth: 140 }, |
| | | { field: 'startTime', title: 'æ´»å¨å¼å§', width: 170 }, |
| | | { field: 'endTime', title: 'æ´»å¨ç»æ', width: 170 }, |
| | | { |
| | | field: 'signupCount', |
| | | title: 'æ¥å/ä¸é', |
| | | width: 110, |
| | | slots: { default: 'signupCount' }, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 90, |
| | | slots: { default: 'status' }, |
| | | }, |
| | | { |
| | | field: 'pushStatus', |
| | | title: 'æ¨é', |
| | | width: 90, |
| | | slots: { default: 'pushStatus' }, |
| | | }, |
| | | { field: 'createTime', title: 'å建æ¶é´', width: 170 }, |
| | | { title: 'æä½', width: 300, fixed: 'right', slots: { default: 'actions' } }, |
| | | ]; |
| | | } |
| | | |
| | | /** æ¥åè®°å½æç´¢è¡¨å */ |
| | | export function useSignupSearchFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'name', |
| | | label: 'å§å', |
| | | component: 'Input', |
| | | componentProps: { allowClear: true, placeholder: '请è¾å
¥å§å' }, |
| | | }, |
| | | { |
| | | fieldName: 'mobile', |
| | | label: 'ææºå·', |
| | | component: 'Input', |
| | | componentProps: { allowClear: true, placeholder: '请è¾å
¥ææºå·' }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: SIGNUP_STATUS_OPTIONS, |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** æ¥åè®°å½åè¡¨åæ®µ */ |
| | | export function useSignupGridColumns(): VxeTableGridOptions<WxCpActivityApi.Signup>['columns'] { |
| | | return [ |
| | | { field: 'name', title: 'å§å', minWidth: 100 }, |
| | | { field: 'mobile', title: 'ææºå·', width: 130 }, |
| | | { field: 'deptName', title: 'é¨é¨', minWidth: 120 }, |
| | | { field: 'wxUserId', title: 'ä¼ä¸å¾®ä¿¡è´¦å·', minWidth: 110 }, |
| | | { |
| | | field: 'content', |
| | | title: 'åä¸è¯´æ', |
| | | minWidth: 160, |
| | | slots: { default: 'content' }, |
| | | }, |
| | | { |
| | | field: 'attachment', |
| | | title: 'éä»¶', |
| | | width: 80, |
| | | slots: { default: 'attachment' }, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 90, |
| | | slots: { default: 'status' }, |
| | | }, |
| | | { field: 'signupTime', title: 'æ¥åæ¶é´', width: 170 }, |
| | | { field: 'checkInTime', title: 'ç¾å°æ¶é´', width: 170 }, |
| | | { title: 'æä½', width: 150, fixed: 'right', slots: { default: 'actions' } }, |
| | | ]; |
| | | } |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { WxCpActivityApi } from '#/api/wxcp/activity'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Button, message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | cancelActivity, |
| | | deleteActivity, |
| | | finishActivity, |
| | | getActivityPage, |
| | | publishActivity, |
| | | } from '#/api/wxcp/activity'; |
| | | |
| | | import { |
| | | ACTIVITY_STATUS, |
| | | ACTIVITY_STATUS_OPTIONS, |
| | | toLabelMap, |
| | | } from '../data'; |
| | | import { |
| | | useActivityGridColumns, |
| | | useActivitySearchFormSchema, |
| | | } from './data'; |
| | | import ActivityForm from './modules/form.vue'; |
| | | import PushModal from './modules/push-modal.vue'; |
| | | import QrCodeModal from './modules/qrcode-modal.vue'; |
| | | import SignupListModal from './modules/signup-list.vue'; |
| | | import StatsModal from './modules/stats-modal.vue'; |
| | | |
| | | defineOptions({ name: 'WxCpActivityList' }); |
| | | |
| | | const statusLabelMap = toLabelMap(ACTIVITY_STATUS_OPTIONS); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: ActivityForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | const [PushModalComp, pushModalApi] = useVbenModal({ |
| | | connectedComponent: PushModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | const [QrCodeModalComp, qrCodeModalApi] = useVbenModal({ |
| | | connectedComponent: QrCodeModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | const [StatsModalComp, statsModalApi] = useVbenModal({ |
| | | connectedComponent: StatsModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | const [SignupModalComp, signupModalApi] = useVbenModal({ |
| | | connectedComponent: SignupListModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | function handleDetail(row: WxCpActivityApi.Activity) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | function handleEdit(row: WxCpActivityApi.Activity) { |
| | | formModalApi.setData({ formType: 'update', id: row.id }).open(); |
| | | } |
| | | |
| | | async function handleDelete(row: WxCpActivityApi.Activity) { |
| | | await deleteActivity(row.id!); |
| | | message.success('å 餿å'); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | async function handlePublish(row: WxCpActivityApi.Activity) { |
| | | await publishActivity(row.id!); |
| | | message.success('å叿åï¼ç°å¨å¯ä»¥æ¨éåçæäºç»´ç äº'); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | async function handleFinish(row: WxCpActivityApi.Activity) { |
| | | await finishActivity(row.id!); |
| | | message.success('å·²ç»æ'); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | async function handleCancel(row: WxCpActivityApi.Activity) { |
| | | await cancelActivity(row.id!); |
| | | message.success('已忶'); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useActivitySearchFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useActivityGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getActivityPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { keyField: 'id', isHover: true }, |
| | | toolbarConfig: { refresh: true, search: true }, |
| | | } as VxeTableGridOptions<WxCpActivityApi.Activity>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <PushModalComp @success="handleRefresh" /> |
| | | <QrCodeModalComp /> |
| | | <StatsModalComp /> |
| | | <SignupModalComp @success="handleRefresh" /> |
| | | |
| | | <Grid table-title="ä¼ä¸å¾®ä¿¡æ´»å¨å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'æ°å»ºæ´»å¨', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['wxcp:activity:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | |
| | | <template #title="{ row }"> |
| | | <Button type="link" class="p-0" @click="handleDetail(row)"> |
| | | {{ row.title }} |
| | | </Button> |
| | | </template> |
| | | |
| | | <template #signupCount="{ row }"> |
| | | <span> |
| | | {{ row.signupCount ?? 0 }} / |
| | | {{ row.signupLimit && row.signupLimit > 0 ? row.signupLimit : 'ä¸é' }} |
| | | </span> |
| | | </template> |
| | | |
| | | <template #status="{ row }"> |
| | | <Tag |
| | | :color=" |
| | | row.status === ACTIVITY_STATUS.PUBLISHED |
| | | ? 'green' |
| | | : row.status === ACTIVITY_STATUS.DRAFT |
| | | ? 'default' |
| | | : row.status === ACTIVITY_STATUS.FINISHED |
| | | ? 'blue' |
| | | : 'red' |
| | | " |
| | | > |
| | | {{ statusLabelMap[row.status!] || '-' }} |
| | | </Tag> |
| | | </template> |
| | | |
| | | <template #pushStatus="{ row }"> |
| | | <Tag :color="row.pushStatus === 1 ? 'green' : 'default'"> |
| | | {{ row.pushStatus === 1 ? 'å·²æ¨é' : 'æªæ¨é' }} |
| | | </Tag> |
| | | </template> |
| | | |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'ç¼è¾', |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['wxcp:activity:update'], |
| | | ifShow: row.status !== ACTIVITY_STATUS.FINISHED && row.status !== ACTIVITY_STATUS.CANCELED, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'åå¸', |
| | | type: 'link', |
| | | auth: ['wxcp:activity:update'], |
| | | ifShow: row.status === ACTIVITY_STATUS.DRAFT, |
| | | popConfirm: { |
| | | title: 'åå¸åæåå³å¯éè¿äºç»´ç æ¥åï¼ç¡®è®¤åå¸ï¼', |
| | | confirm: handlePublish.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: 'æ¨é', |
| | | type: 'link', |
| | | auth: ['wxcp:activity:push'], |
| | | ifShow: row.status === ACTIVITY_STATUS.PUBLISHED, |
| | | onClick: () => pushModalApi.setData({ activity: row }).open(), |
| | | }, |
| | | { |
| | | label: 'äºç»´ç ', |
| | | type: 'link', |
| | | ifShow: row.status === ACTIVITY_STATUS.PUBLISHED, |
| | | onClick: () => qrCodeModalApi.setData({ activity: row }).open(), |
| | | }, |
| | | { |
| | | label: 'ç»è®¡', |
| | | type: 'link', |
| | | auth: ['wxcp:activity:query'], |
| | | onClick: () => statsModalApi.setData({ activity: row }).open(), |
| | | }, |
| | | { |
| | | label: 'æ¥åè®°å½', |
| | | type: 'link', |
| | | auth: ['wxcp:signup:query'], |
| | | onClick: () => signupModalApi.setData({ activity: row }).open(), |
| | | }, |
| | | ]" |
| | | :drop-down-actions="[ |
| | | { |
| | | label: 'ç»æ', |
| | | type: 'link', |
| | | auth: ['wxcp:activity:update'], |
| | | ifShow: row.status === ACTIVITY_STATUS.PUBLISHED, |
| | | popConfirm: { |
| | | title: 'ç»æåæåå°æ æ³åæ¥åï¼ç¡®è®¤ç»æï¼', |
| | | confirm: handleFinish.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: 'åæ¶', |
| | | type: 'link', |
| | | auth: ['wxcp:activity:update'], |
| | | ifShow: row.status === ACTIVITY_STATUS.DRAFT || row.status === ACTIVITY_STATUS.PUBLISHED, |
| | | popConfirm: { |
| | | title: `ç¡®è®¤åæ¶æ´»å¨ã${row.title}ãï¼`, |
| | | confirm: handleCancel.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: 'å é¤', |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['wxcp:activity:delete'], |
| | | ifShow: row.status === ACTIVITY_STATUS.DRAFT, |
| | | popConfirm: { |
| | | title: `确认å 餿´»å¨ã${row.title}ãï¼`, |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { WxCpActivityApi } from '#/api/wxcp/activity'; |
| | | |
| | | import type { ActivityFormType } from '../data'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { Button, message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createActivity, |
| | | getActivity, |
| | | updateActivity, |
| | | } from '#/api/wxcp/activity'; |
| | | |
| | | import { useActivityFormSchema } from '../data'; |
| | | |
| | | defineOptions({ name: 'WxCpActivityForm' }); |
| | | |
| | | const emit = defineEmits<{ success: [] }>(); |
| | | |
| | | const formType = ref<ActivityFormType>('create'); |
| | | const id = ref<number>(); |
| | | const submitting = ref(false); |
| | | |
| | | const isEdit = computed(() => formType.value !== 'detail'); |
| | | const title = computed(() => { |
| | | if (formType.value === 'detail') return 'æ¥çæ´»å¨'; |
| | | if (id.value) return 'ç¼è¾æ´»å¨'; |
| | | return 'æ°å»ºæ´»å¨'; |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 90, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: [], |
| | | showDefaultActions: false, |
| | | wrapperClass: 'grid-cols-2', |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | id.value = undefined; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ formType: ActivityFormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | id.value = data.id; |
| | | formApi.setState({ schema: useActivityFormSchema(formType.value) }); |
| | | formApi.setDisabled(formType.value === 'detail'); |
| | | if (data.id) { |
| | | modalApi.lock(); |
| | | try { |
| | | const detail = await getActivity(data.id); |
| | | await formApi.setValues(detail as unknown as Record<string, unknown>); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | } else { |
| | | await formApi.resetForm(); |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | async function handleSubmit() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | submitting.value = true; |
| | | try { |
| | | const data = (await formApi.getValues()) as WxCpActivityApi.Activity; |
| | | if (id.value) { |
| | | await updateActivity({ ...data, id: id.value }); |
| | | message.success('ä¿®æ¹æå'); |
| | | } else { |
| | | await createActivity(data); |
| | | message.success('å建æåï¼è¯·å¨å表ä¸åå¸å忍é'); |
| | | } |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | } catch (error: unknown) { |
| | | message.error((error as Error)?.message || 'æäº¤å¤±è´¥'); |
| | | } finally { |
| | | submitting.value = false; |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="title" class="w-3/5"> |
| | | <Form class="mx-4" /> |
| | | |
| | | <template #footer> |
| | | <div class="flex justify-end gap-2"> |
| | | <Button @click="modalApi.close()">åæ¶</Button> |
| | | <Button |
| | | v-if="isEdit" |
| | | type="primary" |
| | | :loading="submitting" |
| | | @click="handleSubmit" |
| | | > |
| | | 确认 |
| | | </Button> |
| | | <Button v-else type="primary" @click="modalApi.close()">å
³é</Button> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { WxCpActivityApi } from '#/api/wxcp/activity'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { Alert, Button, message } from 'ant-design-vue'; |
| | | |
| | | import { pushActivity } from '#/api/wxcp/activity'; |
| | | |
| | | import PushScopePicker from '../../components/push-scope-picker.vue'; |
| | | import { PUSH_SCOPE } from '../../data'; |
| | | |
| | | defineOptions({ name: 'WxCpActivityPushModal' }); |
| | | |
| | | const emit = defineEmits<{ success: [] }>(); |
| | | |
| | | const activity = ref<WxCpActivityApi.Activity>({}); |
| | | const scope = ref<number>(PUSH_SCOPE.ALL); |
| | | const target = ref<string>(''); |
| | | const pushing = ref(false); |
| | | const resultMessage = ref(''); |
| | | const resultSuccess = ref(true); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | activity.value = {}; |
| | | resultMessage.value = ''; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ activity: WxCpActivityApi.Activity }>(); |
| | | activity.value = data.activity ?? {}; |
| | | // æ²¿ç¨æ´»å¨ä¸å·²é
ç½®çæ¨éèå´ä¸ç®æ ï¼é¿å
æ¯æ¬¡é½è¦éé |
| | | scope.value = data.activity?.pushScope ?? PUSH_SCOPE.ALL; |
| | | target.value = data.activity?.pushTarget ?? ''; |
| | | }, |
| | | }); |
| | | |
| | | async function handlePush() { |
| | | if (!activity.value.id) { |
| | | return; |
| | | } |
| | | if (scope.value !== PUSH_SCOPE.ALL && !target.value.trim()) { |
| | | message.warning('éæ©ãæå®é¨é¨ãæãæå®æåãæ¶ï¼è¯·å
éæ©æ¨éç®æ '); |
| | | return; |
| | | } |
| | | pushing.value = true; |
| | | resultMessage.value = ''; |
| | | try { |
| | | const res = await pushActivity({ |
| | | id: activity.value.id, |
| | | pushScope: scope.value, |
| | | pushTarget: scope.value === PUSH_SCOPE.ALL ? '' : target.value, |
| | | }); |
| | | resultSuccess.value = !!res?.success; |
| | | if (res?.success) { |
| | | message.success('æ¨éæå'); |
| | | emit('success'); |
| | | await modalApi.close(); |
| | | } else { |
| | | // æ¨éå¤±è´¥ä¸æå¼å¸¸ï¼æä¼ä¸å¾®ä¿¡ç»çåå ç´æ¥å±ç¤ºç»ç¨æ· |
| | | resultMessage.value = res?.message || 'æ¨é失败'; |
| | | } |
| | | } catch (error: unknown) { |
| | | resultSuccess.value = false; |
| | | resultMessage.value = (error as Error)?.message || 'æ¨é失败'; |
| | | } finally { |
| | | pushing.value = false; |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="æ¨éæ´»å¨" class="w-2/5"> |
| | | <div class="px-4"> |
| | | <div class="mb-4 rounded bg-gray-50 p-3 text-sm dark:bg-gray-800"> |
| | | <div class="mb-1 font-medium">{{ activity.title }}</div> |
| | | <div class="text-gray-500"> |
| | | æ¨éåæåçä¼ä¸å¾®ä¿¡ä¼æ¶å°å¡çæ¶æ¯ï¼ç¹å»ãç«å³æ¥åãç´æ¥æå¼æ¥å页ã |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="mb-2 text-sm font-medium">æ¨éèå´</div> |
| | | <PushScopePicker v-model:scope="scope" v-model:target="target" /> |
| | | |
| | | <Alert |
| | | v-if="resultMessage" |
| | | class="mt-4" |
| | | :type="resultSuccess ? 'success' : 'error'" |
| | | :message="resultMessage" |
| | | show-icon |
| | | /> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="flex justify-end gap-2"> |
| | | <Button @click="modalApi.close()">åæ¶</Button> |
| | | <Button type="primary" :loading="pushing" @click="handlePush"> |
| | | 确认æ¨é |
| | | </Button> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { WxCpActivityApi } from '#/api/wxcp/activity'; |
| | | |
| | | import { onUnmounted, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { Button, message, Spin } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | getActivityQrCodeBlob, |
| | | getActivitySignupUrl, |
| | | } from '#/api/wxcp/activity'; |
| | | |
| | | defineOptions({ name: 'WxCpActivityQrCodeModal' }); |
| | | |
| | | const activity = ref<WxCpActivityApi.Activity>({}); |
| | | const qrCodeUrl = ref(''); |
| | | const signupUrl = ref(''); |
| | | const loading = ref(false); |
| | | const loadError = ref(''); |
| | | |
| | | /** éæ¾ä¸ä¸æ¬¡ç objectURLï¼é¿å
å
åæ³æ¼ */ |
| | | function releaseObjectUrl() { |
| | | if (qrCodeUrl.value) { |
| | | URL.revokeObjectURL(qrCodeUrl.value); |
| | | qrCodeUrl.value = ''; |
| | | } |
| | | } |
| | | |
| | | async function loadQrCode(id: number) { |
| | | loading.value = true; |
| | | loadError.value = ''; |
| | | releaseObjectUrl(); |
| | | try { |
| | | // äºç»´ç æ¥å£è¿å PNG äºè¿å¶ï¼ä¸ <img> 带ä¸äºè¯·æ±å¤´ï¼å æ¤èµ° blob + objectURL |
| | | const res = (await getActivityQrCodeBlob(id, 400)) as unknown as { |
| | | data?: Blob; |
| | | }; |
| | | const blob = res?.data instanceof Blob ? res.data : (res as unknown as Blob); |
| | | if (!(blob instanceof Blob)) { |
| | | throw new Error('äºç»´ç è¿åæ ¼å¼å¼å¸¸'); |
| | | } |
| | | qrCodeUrl.value = URL.createObjectURL(blob); |
| | | } catch (error: unknown) { |
| | | loadError.value = (error as Error)?.message || 'äºç»´ç å 载失败'; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function loadSignupUrl(id: number) { |
| | | try { |
| | | signupUrl.value = await getActivitySignupUrl(id); |
| | | } catch { |
| | | // æ¥å龿¥åä¸å°ä¸å½±åäºç»´ç å±ç¤ºï¼éé»å¤ç |
| | | signupUrl.value = ''; |
| | | } |
| | | } |
| | | |
| | | function handleDownload() { |
| | | if (!qrCodeUrl.value || !activity.value.id) { |
| | | return; |
| | | } |
| | | const link = document.createElement('a'); |
| | | link.href = qrCodeUrl.value; |
| | | link.download = `æ´»å¨æ¥åç -${activity.value.title || activity.value.id}.png`; |
| | | document.body.append(link); |
| | | link.click(); |
| | | link.remove(); |
| | | } |
| | | |
| | | async function handleCopyLink() { |
| | | if (!signupUrl.value) { |
| | | message.warning('ææ æ¥å龿¥'); |
| | | return; |
| | | } |
| | | try { |
| | | await navigator.clipboard.writeText(signupUrl.value); |
| | | message.success('æ¥å龿¥å·²å¤å¶'); |
| | | } catch { |
| | | message.warning('å¤å¶å¤±è´¥ï¼è¯·æå¨éä¸é¾æ¥å¤å¶'); |
| | | } |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | activity.value = {}; |
| | | signupUrl.value = ''; |
| | | loadError.value = ''; |
| | | releaseObjectUrl(); |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ activity: WxCpActivityApi.Activity }>(); |
| | | activity.value = data.activity ?? {}; |
| | | if (activity.value.id) { |
| | | loadQrCode(activity.value.id); |
| | | loadSignupUrl(activity.value.id); |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | onUnmounted(releaseObjectUrl); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="æ´»å¨æ¥åäºç»´ç " class="w-2/5"> |
| | | <div class="flex flex-col items-center px-4"> |
| | | <div class="mb-4 text-center text-sm text-gray-500"> |
| | | {{ activity.title }} |
| | | </div> |
| | | |
| | | <Spin :spinning="loading"> |
| | | <div |
| | | class="flex size-70 items-center justify-center rounded border border-dashed border-gray-300" |
| | | > |
| | | <img |
| | | v-if="qrCodeUrl" |
| | | :src="qrCodeUrl" |
| | | alt="æ´»å¨æ¥åäºç»´ç " |
| | | class="size-65 object-contain" |
| | | /> |
| | | <div v-else class="px-6 text-center text-sm text-gray-400"> |
| | | {{ loadError || 'äºç»´ç å è½½ä¸â¦' }} |
| | | </div> |
| | | </div> |
| | | </Spin> |
| | | |
| | | <div class="mt-4 w-full text-xs text-gray-400 break-all"> |
| | | <div v-if="signupUrl">æ¥å龿¥ï¼{{ signupUrl }}</div> |
| | | <div class="mt-2"> |
| | | æåç¨ä¼ä¸å¾®ä¿¡æ«ä¸æ«å³å¯æå¼æ¥å页并æäº¤æ¥åã |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="flex justify-end gap-2"> |
| | | <Button :disabled="!signupUrl" @click="handleCopyLink"> |
| | | å¤å¶æ¥å龿¥ |
| | | </Button> |
| | | <Button type="primary" :disabled="!qrCodeUrl" @click="handleDownload"> |
| | | ä¸è½½äºç»´ç |
| | | </Button> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { WxCpActivityApi } from '#/api/wxcp/activity'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { |
| | | Button, |
| | | Empty, |
| | | message, |
| | | Modal as AntModal, |
| | | Spin, |
| | | Tag, |
| | | Tooltip, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | cancelSignup, |
| | | checkInSignup, |
| | | getSignupAttachments, |
| | | getSignupPage, |
| | | } from '#/api/wxcp/activity'; |
| | | |
| | | import { SIGNUP_STATUS, SIGNUP_STATUS_OPTIONS, toLabelMap } from '../../data'; |
| | | import { useSignupGridColumns, useSignupSearchFormSchema } from '../data'; |
| | | |
| | | defineOptions({ name: 'WxCpActivitySignupList' }); |
| | | |
| | | const emit = defineEmits<{ success: [] }>(); |
| | | |
| | | const statusLabelMap = toLabelMap(SIGNUP_STATUS_OPTIONS); |
| | | |
| | | const activity = ref<WxCpActivityApi.Activity>({}); |
| | | |
| | | /** éä»¶æ¥ç */ |
| | | const attachmentVisible = ref(false); |
| | | const attachmentLoading = ref(false); |
| | | const attachments = ref<WxCpActivityApi.SignupAttachment[]>([]); |
| | | const attachmentTitle = ref(''); |
| | | |
| | | async function handleViewAttachments(row: WxCpActivityApi.Signup) { |
| | | if (!row.id) { |
| | | return; |
| | | } |
| | | attachmentTitle.value = `${row.name || ''} æäº¤çéä»¶`; |
| | | attachmentVisible.value = true; |
| | | attachmentLoading.value = true; |
| | | attachments.value = []; |
| | | try { |
| | | attachments.value = (await getSignupAttachments(row.id)) ?? []; |
| | | } catch (error: unknown) { |
| | | message.error((error as Error)?.message || 'éä»¶å 载失败'); |
| | | } finally { |
| | | attachmentLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | function isImage(item: WxCpActivityApi.SignupAttachment) { |
| | | return (item.contentType || '').startsWith('image/'); |
| | | } |
| | | |
| | | function formatSize(bytes?: number) { |
| | | if (!bytes || bytes <= 0) { |
| | | return ''; |
| | | } |
| | | if (bytes < 1024) return `${bytes} B`; |
| | | if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; |
| | | return `${(bytes / 1024 / 1024).toFixed(1)} MB`; |
| | | } |
| | | |
| | | async function handleCancel(row: WxCpActivityApi.Signup) { |
| | | await cancelSignup(row.id!); |
| | | message.success('å·²åæ¶è¯¥æ¥å'); |
| | | gridApi.query(); |
| | | emit('success'); |
| | | } |
| | | |
| | | async function handleCheckIn(row: WxCpActivityApi.Signup) { |
| | | await checkInSignup(row.id!); |
| | | message.success('ç¾å°æå'); |
| | | gridApi.query(); |
| | | emit('success'); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useSignupSearchFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useSignupGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | if (!activity.value.id) { |
| | | return { list: [], total: 0 }; |
| | | } |
| | | return await getSignupPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | activityId: activity.value.id, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { keyField: 'id', isHover: true }, |
| | | toolbarConfig: { refresh: true, search: true }, |
| | | } as VxeTableGridOptions<WxCpActivityApi.Signup>, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | activity.value = {}; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ activity: WxCpActivityApi.Activity }>(); |
| | | activity.value = data.activity ?? {}; |
| | | // æ´»å¨ID就绪ååæ¥è¯¢ |
| | | gridApi.query(); |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="`æ¥åè®°å½ - ${activity.title || ''}`" class="w-11/12"> |
| | | <div class="px-4"> |
| | | <Grid table-title="æ¥åè®°å½"> |
| | | <template #content="{ row }"> |
| | | <Tooltip v-if="row.content" :title="row.content"> |
| | | <span class="line-clamp-2">{{ row.content }}</span> |
| | | </Tooltip> |
| | | <span v-else class="text-gray-400">-</span> |
| | | </template> |
| | | |
| | | <template #attachment="{ row }"> |
| | | <Button type="link" class="p-0" @click="handleViewAttachments(row)"> |
| | | æ¥ç |
| | | </Button> |
| | | </template> |
| | | |
| | | <template #status="{ row }"> |
| | | <Tag |
| | | :color=" |
| | | row.status === SIGNUP_STATUS.CHECKED_IN |
| | | ? 'green' |
| | | : row.status === SIGNUP_STATUS.CANCELED |
| | | ? 'default' |
| | | : 'blue' |
| | | " |
| | | > |
| | | {{ statusLabelMap[row.status!] || '-' }} |
| | | </Tag> |
| | | </template> |
| | | |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'ç¾å°', |
| | | type: 'link', |
| | | auth: ['wxcp:signup:update'], |
| | | ifShow: row.status === SIGNUP_STATUS.SIGNED_UP, |
| | | popConfirm: { |
| | | title: `确认为ã${row.name}ãç¾å°ï¼`, |
| | | confirm: handleCheckIn.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: 'åæ¶æ¥å', |
| | | type: 'link', |
| | | danger: true, |
| | | auth: ['wxcp:signup:update'], |
| | | ifShow: row.status !== SIGNUP_STATUS.CANCELED, |
| | | popConfirm: { |
| | | title: `ç¡®è®¤åæ¶ã${row.name}ãçæ¥åï¼åæ¶åæ¥å人æ°ä¼ç¸åºåå°ã`, |
| | | confirm: handleCancel.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="flex justify-end"> |
| | | <Button @click="modalApi.close()">å
³é</Button> |
| | | </div> |
| | | </template> |
| | | |
| | | <!-- éä»¶æ¥ç --> |
| | | <AntModal |
| | | v-model:open="attachmentVisible" |
| | | :title="attachmentTitle" |
| | | :footer="null" |
| | | width="640px" |
| | | > |
| | | <Spin :spinning="attachmentLoading"> |
| | | <div v-if="attachments.length" class="space-y-3"> |
| | | <div |
| | | v-for="item in attachments" |
| | | :key="item.id" |
| | | class="rounded border border-gray-200 p-3 dark:border-gray-700" |
| | | > |
| | | <div class="mb-2 text-sm break-all"> |
| | | {{ item.originalFilename || item.name || `éä»¶${item.id}` }} |
| | | <span class="ml-2 text-xs text-gray-400"> |
| | | {{ formatSize(item.byteSize) }} |
| | | </span> |
| | | </div> |
| | | <a |
| | | v-if="isImage(item)" |
| | | :href="item.previewURL || item.url" |
| | | target="_blank" |
| | | rel="noopener noreferrer" |
| | | > |
| | | <img |
| | | :src="item.previewURL || item.url" |
| | | alt="éä»¶é¢è§" |
| | | class="max-h-40 rounded object-contain" |
| | | /> |
| | | </a> |
| | | <a |
| | | :href="item.downloadURL || item.url" |
| | | target="_blank" |
| | | rel="noopener noreferrer" |
| | | class="text-xs text-blue-500 hover:underline" |
| | | > |
| | | ä¸è½½ |
| | | </a> |
| | | </div> |
| | | </div> |
| | | <Empty v-else-if="!attachmentLoading" description="该æ¥åæªä¸ä¼ éä»¶" /> |
| | | </Spin> |
| | | </AntModal> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { WxCpActivityApi } from '#/api/wxcp/activity'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { Progress, Spin } from 'ant-design-vue'; |
| | | |
| | | import { getActivityStats } from '#/api/wxcp/activity'; |
| | | |
| | | defineOptions({ name: 'WxCpActivityStatsModal' }); |
| | | |
| | | const activity = ref<WxCpActivityApi.Activity>({}); |
| | | const stats = ref<null | WxCpActivityApi.ActivityStats>(null); |
| | | const loading = ref(false); |
| | | const loadError = ref(''); |
| | | |
| | | /** ä¸éå颿¶ä¸å±ç¤ºè¿åº¦æ¡ */ |
| | | const hasLimit = computed( |
| | | () => !!stats.value?.signupLimit && stats.value.signupLimit > 0, |
| | | ); |
| | | |
| | | async function loadStats(id: number) { |
| | | loading.value = true; |
| | | loadError.value = ''; |
| | | try { |
| | | stats.value = await getActivityStats(id); |
| | | } catch (error: unknown) { |
| | | loadError.value = (error as Error)?.message || 'ç»è®¡å 载失败'; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | stats.value = null; |
| | | activity.value = {}; |
| | | loadError.value = ''; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ activity: WxCpActivityApi.Activity }>(); |
| | | activity.value = data.activity ?? {}; |
| | | if (activity.value.id) { |
| | | loadStats(activity.value.id); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="æ¥åç»è®¡" class="w-3/5"> |
| | | <Spin :spinning="loading"> |
| | | <div class="px-4"> |
| | | <div class="mb-3 text-sm text-gray-500"> |
| | | {{ stats?.activityTitle || activity.title }} |
| | | </div> |
| | | |
| | | <div v-if="loadError" class="py-6 text-center text-sm text-red-500"> |
| | | {{ loadError }} |
| | | </div> |
| | | |
| | | <template v-else-if="stats"> |
| | | <!-- å
³é®ææ --> |
| | | <div class="mb-4 grid grid-cols-4 gap-3"> |
| | | <div class="rounded bg-gray-50 p-3 text-center dark:bg-gray-800"> |
| | | <div class="text-xl font-semibold"> |
| | | {{ stats.totalSignup }} |
| | | </div> |
| | | <div class="mt-1 text-xs text-gray-500">æææ¥å</div> |
| | | </div> |
| | | <div class="rounded bg-gray-50 p-3 text-center dark:bg-gray-800"> |
| | | <div class="text-xl font-semibold"> |
| | | {{ stats.signedUpCount }} |
| | | </div> |
| | | <div class="mt-1 text-xs text-gray-500">æªç¾å°</div> |
| | | </div> |
| | | <div class="rounded bg-gray-50 p-3 text-center dark:bg-gray-800"> |
| | | <div class="text-xl font-semibold"> |
| | | {{ stats.checkedInCount }} |
| | | </div> |
| | | <div class="mt-1 text-xs text-gray-500">å·²ç¾å°</div> |
| | | </div> |
| | | <div class="rounded bg-gray-50 p-3 text-center dark:bg-gray-800"> |
| | | <div class="text-xl font-semibold"> |
| | | {{ stats.canceledCount }} |
| | | </div> |
| | | <div class="mt-1 text-xs text-gray-500">已忶</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- è¿åº¦ --> |
| | | <div v-if="hasLimit" class="mb-5"> |
| | | <div class="mb-1 flex justify-between text-xs text-gray-500"> |
| | | <span>æ¥åè¿åº¦</span> |
| | | <span> |
| | | {{ stats.totalSignup }} / {{ stats.signupLimit }} |
| | | ï¼å©ä½ {{ stats.remaining ?? 0 }}ï¼ |
| | | </span> |
| | | </div> |
| | | <Progress :percent="stats.signupPercent ?? 0" /> |
| | | </div> |
| | | <div v-else class="mb-5 text-xs text-gray-500"> |
| | | æ¬æ¬¡æ´»å¨ä¸éåé¢ï¼å½åå·²æ¥å {{ stats.totalSignup }} 人 |
| | | </div> |
| | | |
| | | <!-- åå¸ --> |
| | | <div class="grid grid-cols-3 gap-4 text-sm"> |
| | | <div> |
| | | <div class="mb-2 font-medium">æé¨é¨</div> |
| | | <div v-if="stats.deptStats?.length" class="space-y-1"> |
| | | <div |
| | | v-for="item in stats.deptStats" |
| | | :key="item.name" |
| | | class="flex justify-between text-gray-600" |
| | | > |
| | | <span class="truncate">{{ item.name }}</span> |
| | | <span>{{ item.count }}</span> |
| | | </div> |
| | | </div> |
| | | <div v-else class="text-xs text-gray-400">ææ æ°æ®</div> |
| | | </div> |
| | | |
| | | <div> |
| | | <div class="mb-2 font-medium">ææ¥åæ¥æ</div> |
| | | <div v-if="stats.dateStats?.length" class="space-y-1"> |
| | | <div |
| | | v-for="item in stats.dateStats" |
| | | :key="item.name" |
| | | class="flex justify-between text-gray-600" |
| | | > |
| | | <span>{{ item.name }}</span> |
| | | <span>{{ item.count }}</span> |
| | | </div> |
| | | </div> |
| | | <div v-else class="text-xs text-gray-400">ææ æ°æ®</div> |
| | | </div> |
| | | |
| | | <div> |
| | | <div class="mb-2 font-medium">ææ¥æº</div> |
| | | <div v-if="stats.sourceStats?.length" class="space-y-1"> |
| | | <div |
| | | v-for="item in stats.sourceStats" |
| | | :key="item.name" |
| | | class="flex justify-between text-gray-600" |
| | | > |
| | | <span>{{ item.name }}</span> |
| | | <span>{{ item.count }}</span> |
| | | </div> |
| | | </div> |
| | | <div v-else class="text-xs text-gray-400">ææ æ°æ®</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="mt-4 text-xs text-gray-400"> |
| | | ç¾å°ç {{ stats.checkInPercent }}%ï¼ææææ¥åæ°è®¡ç®ï¼ä¸å«å·²åæ¶ï¼ |
| | | </div> |
| | | </template> |
| | | </div> |
| | | </Spin> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { WxCpMemberApi } from '#/api/wxcp/member'; |
| | | |
| | | import { computed, onMounted, ref, watch } from 'vue'; |
| | | |
| | | import { Alert, Radio, Select, Spin } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | getWxCpDepartmentList, |
| | | getWxCpMemberList, |
| | | } from '#/api/wxcp/member'; |
| | | |
| | | import { PUSH_SCOPE_OPTIONS, PUSH_SCOPE } from '../data'; |
| | | |
| | | defineOptions({ name: 'WxCpPushScopePicker' }); |
| | | |
| | | interface Props { |
| | | /** æ¨éèå´ï¼0å
¨å 1æå®é¨é¨ 2æå®æå */ |
| | | scope?: number; |
| | | /** æ¨éç®æ ï¼éå·åéï¼èå´æ¯å
¨åæ¶æ æä¹ */ |
| | | target?: string; |
| | | disabled?: boolean; |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<Props>(), { |
| | | scope: PUSH_SCOPE.ALL, |
| | | target: '', |
| | | disabled: false, |
| | | }); |
| | | |
| | | const emit = defineEmits<{ |
| | | 'update:scope': [value: number]; |
| | | 'update:target': [value: string]; |
| | | }>(); |
| | | |
| | | /** éè®¯å½æ°æ®ï¼åªå¨é¦æ¬¡éè¦æ¶æåï¼é¿å
æ¯æ¬¡æå¼å¼¹çªé½æä¼ä¸å¾®ä¿¡æ¥å£ */ |
| | | const departments = ref<WxCpMemberApi.Department[]>([]); |
| | | const members = ref<WxCpMemberApi.Member[]>([]); |
| | | const loading = ref(false); |
| | | const loaded = ref(false); |
| | | const loadError = ref(''); |
| | | |
| | | async function ensureContacts() { |
| | | if (loaded.value || loading.value) { |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | loadError.value = ''; |
| | | try { |
| | | const [deptList, memberList] = await Promise.all([ |
| | | getWxCpDepartmentList(), |
| | | getWxCpMemberList({ fetchChild: true }), |
| | | ]); |
| | | departments.value = deptList ?? []; |
| | | members.value = memberList ?? []; |
| | | loaded.value = true; |
| | | } catch (error: unknown) { |
| | | loadError.value = |
| | | (error as Error)?.message || 'å è½½ä¼ä¸å¾®ä¿¡é讯å½å¤±è´¥ï¼è¯·ç¨åéè¯'; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | // éå°éè¦é讯å½çèå´æ¶æå è½½ |
| | | watch( |
| | | () => props.scope, |
| | | (scope) => { |
| | | if (scope === PUSH_SCOPE.PARTY || scope === PUSH_SCOPE.USER) { |
| | | ensureContacts(); |
| | | } |
| | | }, |
| | | ); |
| | | |
| | | onMounted(() => { |
| | | if (props.scope === PUSH_SCOPE.PARTY || props.scope === PUSH_SCOPE.USER) { |
| | | ensureContacts(); |
| | | } |
| | | }); |
| | | |
| | | const deptOptions = computed(() => |
| | | departments.value.map((d) => ({ label: d.name || `é¨é¨${d.id}`, value: d.id })), |
| | | ); |
| | | |
| | | const memberOptions = computed(() => |
| | | members.value.map((m) => ({ |
| | | label: m.name ? `${m.name}ï¼${m.userId}ï¼` : m.userId, |
| | | value: m.userId, |
| | | })), |
| | | ); |
| | | |
| | | /** ç®æ å符串 <-> æ°ç» */ |
| | | const selected = computed<string[] | number[]>({ |
| | | get() { |
| | | const raw = (props.target || '').trim(); |
| | | if (!raw) { |
| | | return []; |
| | | } |
| | | const parts = raw |
| | | .split(/[,ï¼]/) |
| | | .map((s) => s.trim()) |
| | | .filter(Boolean); |
| | | // é¨é¨ç¨æ°åï¼æåç¨åç¬¦ä¸²ï¼æåè´¦å·æ¯ LiuYi è¿ç§ï¼ |
| | | return props.scope === PUSH_SCOPE.PARTY |
| | | ? parts.map((p) => Number(p)).filter((n) => Number.isFinite(n)) |
| | | : parts; |
| | | }, |
| | | set(value) { |
| | | emit('update:target', (value as (number | string)[]).join(',')); |
| | | }, |
| | | }); |
| | | |
| | | function handleScopeChange(e: unknown) { |
| | | const value = Number((e as { target?: { value?: number } })?.target?.value); |
| | | emit('update:scope', Number.isFinite(value) ? value : PUSH_SCOPE.ALL); |
| | | // 忢èå´æ¶æ¸
ææ§ç®æ ï¼é¿å
ææåè´¦å·å½æé¨é¨IDæäº¤ |
| | | emit('update:target', ''); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div> |
| | | <Radio.Group |
| | | :value="scope" |
| | | :disabled="disabled" |
| | | button-style="solid" |
| | | @change="handleScopeChange" |
| | | > |
| | | <Radio.Button |
| | | v-for="opt in PUSH_SCOPE_OPTIONS" |
| | | :key="opt.value" |
| | | :value="opt.value" |
| | | > |
| | | {{ opt.label }} |
| | | </Radio.Button> |
| | | </Radio.Group> |
| | | |
| | | <div v-if="scope !== PUSH_SCOPE.ALL" class="mt-3"> |
| | | <Spin :spinning="loading"> |
| | | <Alert |
| | | v-if="loadError" |
| | | :message="loadError" |
| | | type="warning" |
| | | show-icon |
| | | class="mb-2" |
| | | /> |
| | | <Select |
| | | v-model:value="selected" |
| | | :options="scope === PUSH_SCOPE.PARTY ? deptOptions : memberOptions" |
| | | :disabled="disabled || !!loadError" |
| | | :mode="scope === PUSH_SCOPE.PARTY ? 'multiple' : 'multiple'" |
| | | :placeholder=" |
| | | scope === PUSH_SCOPE.PARTY |
| | | ? 'è¯·éæ©é¨é¨ï¼å¯å¤é' |
| | | : 'è¯·éæ©æåï¼å¯å¤é' |
| | | " |
| | | option-filter-prop="label" |
| | | show-search |
| | | allow-clear |
| | | class="w-full" |
| | | /> |
| | | </Spin> |
| | | <div class="mt-2 text-xs text-gray-400"> |
| | | {{ |
| | | scope === PUSH_SCOPE.PARTY |
| | | ? 'æ¨éç»æéé¨é¨ä¸çå
¨é¨æå' |
| | | : 'æ¨éç»æéæåï¼è´¦å·æ¥èªä¼ä¸å¾®ä¿¡é讯å½' |
| | | }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * ä¼ä¸å¾®ä¿¡æ¨¡åå
±äº«å¸¸é |
| | | * |
| | | * å¼ä¸å端æä¸¾ä¸ä¸å¯¹åºï¼æ¹å¨æ¶ä¸¤è¾¹å¿
é¡»åæ¥ï¼ |
| | | * - WxCpPushScopeEnumï¼0å
¨å 1æå®é¨é¨ 2æå®æå |
| | | * - WxCpActivityStatusEnumï¼0è稿 1å·²åå¸ 2å·²ç»æ 3已忶 |
| | | * - WxCpNoticeStatusEnumï¼0è稿 1å·²åå¸ 2å·²æ¤å |
| | | * - WxCpSignupStatusEnumï¼0å·²æ¥å 1已忶 2å·²ç¾å° |
| | | */ |
| | | |
| | | /** æ¨éèå´ */ |
| | | export const PUSH_SCOPE = { |
| | | ALL: 0, |
| | | PARTY: 1, |
| | | USER: 2, |
| | | } as const; |
| | | |
| | | /** æ¨éèå´é项 */ |
| | | export const PUSH_SCOPE_OPTIONS = [ |
| | | { label: 'å
¨å', value: PUSH_SCOPE.ALL }, |
| | | { label: 'æå®é¨é¨', value: PUSH_SCOPE.PARTY }, |
| | | { label: 'æå®æå', value: PUSH_SCOPE.USER }, |
| | | ]; |
| | | |
| | | /** æ¨éç¶æï¼æ´»å¨ä¸éç¥å
±ç¨ï¼ */ |
| | | export const PUSH_STATUS_OPTIONS = [ |
| | | { label: 'æªæ¨é', value: 0 }, |
| | | { label: 'å·²æ¨é', value: 1 }, |
| | | ]; |
| | | |
| | | /** æ´»å¨ç¶æ */ |
| | | export const ACTIVITY_STATUS = { |
| | | DRAFT: 0, |
| | | PUBLISHED: 1, |
| | | FINISHED: 2, |
| | | CANCELED: 3, |
| | | } as const; |
| | | |
| | | /** æ´»å¨ç¶æé项 */ |
| | | export const ACTIVITY_STATUS_OPTIONS = [ |
| | | { label: 'è稿', value: ACTIVITY_STATUS.DRAFT }, |
| | | { label: 'å·²åå¸', value: ACTIVITY_STATUS.PUBLISHED }, |
| | | { label: 'å·²ç»æ', value: ACTIVITY_STATUS.FINISHED }, |
| | | { label: '已忶', value: ACTIVITY_STATUS.CANCELED }, |
| | | ]; |
| | | |
| | | /** éç¥ç¶æ */ |
| | | export const NOTICE_STATUS = { |
| | | DRAFT: 0, |
| | | PUBLISHED: 1, |
| | | REVOKED: 2, |
| | | } as const; |
| | | |
| | | /** éç¥ç¶æé项 */ |
| | | export const NOTICE_STATUS_OPTIONS = [ |
| | | { label: 'è稿', value: NOTICE_STATUS.DRAFT }, |
| | | { label: 'å·²åå¸', value: NOTICE_STATUS.PUBLISHED }, |
| | | { label: 'å·²æ¤å', value: NOTICE_STATUS.REVOKED }, |
| | | ]; |
| | | |
| | | /** æ¥åç¶æ */ |
| | | export const SIGNUP_STATUS = { |
| | | SIGNED_UP: 0, |
| | | CANCELED: 1, |
| | | CHECKED_IN: 2, |
| | | } as const; |
| | | |
| | | /** æ¥åç¶æé项 */ |
| | | export const SIGNUP_STATUS_OPTIONS = [ |
| | | { label: 'å·²æ¥å', value: SIGNUP_STATUS.SIGNED_UP }, |
| | | { label: '已忶', value: SIGNUP_STATUS.CANCELED }, |
| | | { label: 'å·²ç¾å°', value: SIGNUP_STATUS.CHECKED_IN }, |
| | | ]; |
| | | |
| | | /** æé项æ°ç»è½¬æ value -> label çæ å°ï¼ä¾¿äºè¡¨æ ¼éç´æ¥å䏿å */ |
| | | export function toLabelMap( |
| | | options: Array<{ label: string; value: number }>, |
| | | ): Record<number, string> { |
| | | return Object.fromEntries(options.map((o) => [o.value, o.label])); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { WxCpNoticeApi } from '#/api/wxcp/notice'; |
| | | |
| | | import { NOTICE_STATUS_OPTIONS, PUSH_STATUS_OPTIONS } from '../data'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type NoticeFormType = 'create' | 'update' | 'detail'; |
| | | |
| | | /** éç¥è¡¨å */ |
| | | export function useNoticeFormSchema(formType: NoticeFormType): VbenFormSchema[] { |
| | | const readonly = formType === 'detail'; |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'title', |
| | | label: 'éç¥æ é¢', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥éç¥æ é¢', disabled: readonly }, |
| | | rules: 'required', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'content', |
| | | label: 'éç¥æ£æ', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | rows: 8, |
| | | placeholder: '请è¾å
¥éç¥æ£æ', |
| | | disabled: readonly, |
| | | }, |
| | | rules: 'required', |
| | | help: 'æ¨éæ¶è¶
è¿ 600 åä¼èªå¨æªæï¼å»ºè®®æå
³é®ä¿¡æ¯æ¾åé¢', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { rows: 2, placeholder: 'å
é¨å¤æ³¨ï¼ä¸æ¨é', disabled: readonly }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** éç¥æç´¢è¡¨å */ |
| | | export function useNoticeSearchFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'title', |
| | | label: 'éç¥æ é¢', |
| | | component: 'Input', |
| | | componentProps: { allowClear: true, placeholder: '请è¾å
¥éç¥æ é¢' }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: NOTICE_STATUS_OPTIONS, |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'pushStatus', |
| | | label: 'æ¨éç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: PUSH_STATUS_OPTIONS, |
| | | placeholder: 'è¯·éæ©æ¨éç¶æ', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** éç¥åè¡¨åæ®µ */ |
| | | export function useNoticeGridColumns(): VxeTableGridOptions<WxCpNoticeApi.Notice>['columns'] { |
| | | return [ |
| | | { |
| | | field: 'title', |
| | | title: 'éç¥æ é¢', |
| | | minWidth: 220, |
| | | slots: { default: 'title' }, |
| | | }, |
| | | { |
| | | field: 'content', |
| | | title: 'æ£ææè¦', |
| | | minWidth: 260, |
| | | slots: { default: 'content' }, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 90, |
| | | slots: { default: 'status' }, |
| | | }, |
| | | { |
| | | field: 'pushScopeName', |
| | | title: 'æ¨éèå´', |
| | | width: 110, |
| | | }, |
| | | { |
| | | field: 'pushStatus', |
| | | title: 'æ¨é', |
| | | width: 90, |
| | | slots: { default: 'pushStatus' }, |
| | | }, |
| | | { field: 'pushTime', title: 'æ¨éæ¶é´', width: 170 }, |
| | | { field: 'createTime', title: 'å建æ¶é´', width: 170 }, |
| | | { title: 'æä½', width: 220, fixed: 'right', slots: { default: 'actions' } }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { WxCpNoticeApi } from '#/api/wxcp/notice'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Button, message, Tag, Tooltip } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteNotice, |
| | | getNoticePage, |
| | | publishNotice, |
| | | revokeNotice, |
| | | } from '#/api/wxcp/notice'; |
| | | |
| | | import { NOTICE_STATUS, NOTICE_STATUS_OPTIONS, toLabelMap } from '../data'; |
| | | import { |
| | | useNoticeGridColumns, |
| | | useNoticeSearchFormSchema, |
| | | } from './data'; |
| | | import NoticeForm from './modules/form.vue'; |
| | | import NoticePushModal from './modules/push-modal.vue'; |
| | | |
| | | defineOptions({ name: 'WxCpNoticeList' }); |
| | | |
| | | const statusLabelMap = toLabelMap(NOTICE_STATUS_OPTIONS); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: NoticeForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | const [PushModalComp, pushModalApi] = useVbenModal({ |
| | | connectedComponent: NoticePushModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | function handleDetail(row: WxCpNoticeApi.Notice) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | function handleEdit(row: WxCpNoticeApi.Notice) { |
| | | formModalApi.setData({ formType: 'update', id: row.id }).open(); |
| | | } |
| | | |
| | | async function handleDelete(row: WxCpNoticeApi.Notice) { |
| | | await deleteNotice(row.id!); |
| | | message.success('å 餿å'); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | async function handlePublish(row: WxCpNoticeApi.Notice) { |
| | | await publishNotice(row.id!); |
| | | message.success('å叿åï¼ç°å¨å¯ä»¥æ¨éäº'); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | async function handleRevoke(row: WxCpNoticeApi.Notice) { |
| | | await revokeNotice(row.id!); |
| | | message.success('å·²æ¤å'); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useNoticeSearchFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useNoticeGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getNoticePage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { keyField: 'id', isHover: true }, |
| | | toolbarConfig: { refresh: true, search: true }, |
| | | } as VxeTableGridOptions<WxCpNoticeApi.Notice>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <PushModalComp @success="handleRefresh" /> |
| | | |
| | | <Grid table-title="ä¼ä¸å¾®ä¿¡éç¥å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'æ°å»ºéç¥', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['wxcp:notice:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | |
| | | <template #title="{ row }"> |
| | | <Button type="link" class="p-0" @click="handleDetail(row)"> |
| | | {{ row.title }} |
| | | </Button> |
| | | </template> |
| | | |
| | | <template #content="{ row }"> |
| | | <Tooltip v-if="row.content" :title="row.content"> |
| | | <span class="line-clamp-2">{{ row.content }}</span> |
| | | </Tooltip> |
| | | <span v-else class="text-gray-400">-</span> |
| | | </template> |
| | | |
| | | <template #status="{ row }"> |
| | | <Tag |
| | | :color=" |
| | | row.status === NOTICE_STATUS.PUBLISHED |
| | | ? 'green' |
| | | : row.status === NOTICE_STATUS.DRAFT |
| | | ? 'default' |
| | | : 'orange' |
| | | " |
| | | > |
| | | {{ statusLabelMap[row.status!] || '-' }} |
| | | </Tag> |
| | | </template> |
| | | |
| | | <template #pushStatus="{ row }"> |
| | | <Tag :color="row.pushStatus === 1 ? 'green' : 'default'"> |
| | | {{ row.pushStatus === 1 ? 'å·²æ¨é' : 'æªæ¨é' }} |
| | | </Tag> |
| | | </template> |
| | | |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'ç¼è¾', |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['wxcp:notice:update'], |
| | | ifShow: row.status !== NOTICE_STATUS.REVOKED, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'åå¸', |
| | | type: 'link', |
| | | auth: ['wxcp:notice:update'], |
| | | ifShow: row.status === NOTICE_STATUS.DRAFT, |
| | | popConfirm: { |
| | | title: 'åå¸åå³å¯æ¨éç»æåï¼ç¡®è®¤åå¸ï¼', |
| | | confirm: handlePublish.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: 'æ¨é', |
| | | type: 'link', |
| | | auth: ['wxcp:notice:push'], |
| | | ifShow: row.status === NOTICE_STATUS.PUBLISHED, |
| | | onClick: () => pushModalApi.setData({ notice: row }).open(), |
| | | }, |
| | | { |
| | | label: 'æ¤å', |
| | | type: 'link', |
| | | auth: ['wxcp:notice:update'], |
| | | ifShow: row.status === NOTICE_STATUS.PUBLISHED, |
| | | popConfirm: { |
| | | title: 'æ¤åå该éç¥ä¸è½åæ¨éï¼ç¡®è®¤æ¤åï¼', |
| | | confirm: handleRevoke.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: 'å é¤', |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['wxcp:notice:delete'], |
| | | popConfirm: { |
| | | title: `确认å é¤éç¥ã${row.title}ãï¼`, |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { WxCpNoticeApi } from '#/api/wxcp/notice'; |
| | | |
| | | import type { NoticeFormType } from '../data'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { Button, message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createNotice, |
| | | getNotice, |
| | | updateNotice, |
| | | } from '#/api/wxcp/notice'; |
| | | |
| | | import { useNoticeFormSchema } from '../data'; |
| | | |
| | | defineOptions({ name: 'WxCpNoticeForm' }); |
| | | |
| | | const emit = defineEmits<{ success: [] }>(); |
| | | |
| | | const formType = ref<NoticeFormType>('create'); |
| | | const id = ref<number>(); |
| | | const submitting = ref(false); |
| | | |
| | | const isEdit = computed(() => formType.value !== 'detail'); |
| | | const title = computed(() => { |
| | | if (formType.value === 'detail') return 'æ¥çéç¥'; |
| | | if (id.value) return 'ç¼è¾éç¥'; |
| | | return 'æ°å»ºéç¥'; |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 90, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: [], |
| | | showDefaultActions: false, |
| | | wrapperClass: 'grid-cols-1', |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | id.value = undefined; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ formType: NoticeFormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | id.value = data.id; |
| | | formApi.setState({ schema: useNoticeFormSchema(formType.value) }); |
| | | formApi.setDisabled(formType.value === 'detail'); |
| | | if (data.id) { |
| | | modalApi.lock(); |
| | | try { |
| | | const detail = await getNotice(data.id); |
| | | await formApi.setValues(detail as unknown as Record<string, unknown>); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | } else { |
| | | await formApi.resetForm(); |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | async function handleSubmit() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | submitting.value = true; |
| | | try { |
| | | const data = (await formApi.getValues()) as WxCpNoticeApi.Notice; |
| | | if (id.value) { |
| | | await updateNotice({ ...data, id: id.value }); |
| | | message.success('ä¿®æ¹æå'); |
| | | } else { |
| | | await createNotice(data); |
| | | message.success('å建æåï¼è¯·å¨å表ä¸åå¸å忍é'); |
| | | } |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | } catch (error: unknown) { |
| | | message.error((error as Error)?.message || 'æäº¤å¤±è´¥'); |
| | | } finally { |
| | | submitting.value = false; |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="title" class="w-3/5"> |
| | | <Form class="mx-4" /> |
| | | |
| | | <template #footer> |
| | | <div class="flex justify-end gap-2"> |
| | | <Button @click="modalApi.close()">åæ¶</Button> |
| | | <Button |
| | | v-if="isEdit" |
| | | type="primary" |
| | | :loading="submitting" |
| | | @click="handleSubmit" |
| | | > |
| | | 确认 |
| | | </Button> |
| | | <Button v-else type="primary" @click="modalApi.close()">å
³é</Button> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { WxCpNoticeApi } from '#/api/wxcp/notice'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { Alert, Button, message } from 'ant-design-vue'; |
| | | |
| | | import { pushNotice } from '#/api/wxcp/notice'; |
| | | |
| | | import PushScopePicker from '../../components/push-scope-picker.vue'; |
| | | import { PUSH_SCOPE } from '../../data'; |
| | | |
| | | defineOptions({ name: 'WxCpNoticePushModal' }); |
| | | |
| | | const emit = defineEmits<{ success: [] }>(); |
| | | |
| | | const notice = ref<WxCpNoticeApi.Notice>({}); |
| | | const scope = ref<number>(PUSH_SCOPE.ALL); |
| | | const target = ref<string>(''); |
| | | const pushing = ref(false); |
| | | const resultMessage = ref(''); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | notice.value = {}; |
| | | resultMessage.value = ''; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ notice: WxCpNoticeApi.Notice }>(); |
| | | notice.value = data.notice ?? {}; |
| | | scope.value = data.notice?.pushScope ?? PUSH_SCOPE.ALL; |
| | | target.value = data.notice?.pushTarget ?? ''; |
| | | }, |
| | | }); |
| | | |
| | | async function handlePush() { |
| | | if (!notice.value.id) { |
| | | return; |
| | | } |
| | | if (scope.value !== PUSH_SCOPE.ALL && !target.value.trim()) { |
| | | message.warning('éæ©ãæå®é¨é¨ãæãæå®æåãæ¶ï¼è¯·å
éæ©æ¨éç®æ '); |
| | | return; |
| | | } |
| | | pushing.value = true; |
| | | resultMessage.value = ''; |
| | | try { |
| | | const res = await pushNotice({ |
| | | id: notice.value.id, |
| | | pushScope: scope.value, |
| | | pushTarget: scope.value === PUSH_SCOPE.ALL ? '' : target.value, |
| | | }); |
| | | if (res?.success) { |
| | | message.success('æ¨éæå'); |
| | | emit('success'); |
| | | await modalApi.close(); |
| | | } else { |
| | | resultMessage.value = res?.message || 'æ¨é失败'; |
| | | } |
| | | } catch (error: unknown) { |
| | | resultMessage.value = (error as Error)?.message || 'æ¨é失败'; |
| | | } finally { |
| | | pushing.value = false; |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="æ¨ééç¥" class="w-2/5"> |
| | | <div class="px-4"> |
| | | <div class="mb-4 rounded bg-gray-50 p-3 text-sm dark:bg-gray-800"> |
| | | <div class="mb-1 font-medium">{{ notice.title }}</div> |
| | | <div class="text-gray-500"> |
| | | æ¨éåæåçä¼ä¸å¾®ä¿¡ä¼æ¶å°çº¯ææ¬æ¶æ¯ï¼å
容è¶
è¿ 600 åä¼èªå¨æªæã |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="mb-2 text-sm font-medium">æ¨éèå´</div> |
| | | <PushScopePicker v-model:scope="scope" v-model:target="target" /> |
| | | |
| | | <Alert |
| | | v-if="resultMessage" |
| | | class="mt-4" |
| | | type="error" |
| | | :message="resultMessage" |
| | | show-icon |
| | | /> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="flex justify-end gap-2"> |
| | | <Button @click="modalApi.close()">åæ¶</Button> |
| | | <Button type="primary" :loading="pushing" @click="handlePush"> |
| | | 确认æ¨é |
| | | </Button> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |