Commit fa6e5b55 by Sweet Zhang

封装接口

parent 26f467d0
VITE_API_BASE_URL='http://139.224.145.34:9002/email/api'
\ No newline at end of file
VITE_API_BASE_URL=/email/api
\ No newline at end of file
...@@ -9,6 +9,8 @@ ...@@ -9,6 +9,8 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^7.0.1", "@fortawesome/fontawesome-free": "^7.0.1",
"axios": "^1.12.2",
"element-plus": "^2.11.3",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"vue": "^3.5.18", "vue": "^3.5.18",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
...@@ -649,6 +651,24 @@ ...@@ -649,6 +651,24 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@ctrl/tinycolor": {
"version": "3.6.1",
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
"integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@element-plus/icons-vue": {
"version": "2.3.2",
"resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
"integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
"license": "MIT",
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.25.10", "version": "0.25.10",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz",
...@@ -1267,6 +1287,31 @@ ...@@ -1267,6 +1287,31 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@floating-ui/core": {
"version": "1.7.3",
"resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.3.tgz",
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.4",
"resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.4.tgz",
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.3",
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@fortawesome/fontawesome-free": { "node_modules/@fortawesome/fontawesome-free": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmmirror.com/@fortawesome/fontawesome-free/-/fontawesome-free-7.0.1.tgz", "resolved": "https://registry.npmmirror.com/@fortawesome/fontawesome-free/-/fontawesome-free-7.0.1.tgz",
...@@ -1487,6 +1532,17 @@ ...@@ -1487,6 +1532,17 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@popperjs/core": {
"name": "@sxzz/popperjs-es",
"version": "2.11.7",
"resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
"integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rolldown/pluginutils": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.29", "version": "1.0.0-beta.29",
"resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz", "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz",
...@@ -1872,6 +1928,21 @@ ...@@ -1872,6 +1928,21 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
"license": "MIT"
},
"node_modules/@types/lodash-es": {
"version": "4.17.12",
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.18.6", "version": "22.18.6",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-22.18.6.tgz", "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.18.6.tgz",
...@@ -1889,6 +1960,12 @@ ...@@ -1889,6 +1960,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/web-bluetooth": {
"version": "0.0.16",
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
"integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.44.0", "version": "8.44.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.0.tgz", "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.0.tgz",
...@@ -2746,6 +2823,94 @@ ...@@ -2746,6 +2823,94 @@
} }
} }
}, },
"node_modules/@vueuse/core": {
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz",
"integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.16",
"@vueuse/metadata": "9.13.0",
"@vueuse/shared": "9.13.0",
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/core/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vueuse/metadata": {
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz",
"integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz",
"integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
"license": "MIT",
"dependencies": {
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/abbrev": { "node_modules/abbrev": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmmirror.com/abbrev/-/abbrev-2.0.0.tgz", "resolved": "https://registry.npmmirror.com/abbrev/-/abbrev-2.0.0.tgz",
...@@ -2897,6 +3062,18 @@ ...@@ -2897,6 +3062,18 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/autoprefixer": { "node_modules/autoprefixer": {
"version": "10.4.21", "version": "10.4.21",
"resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.21.tgz", "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.21.tgz",
...@@ -2935,6 +3112,17 @@ ...@@ -2935,6 +3112,17 @@
"postcss": "^8.1.0" "postcss": "^8.1.0"
} }
}, },
"node_modules/axios": {
"version": "1.12.2",
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
...@@ -3064,6 +3252,19 @@ ...@@ -3064,6 +3252,19 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": { "node_modules/callsites": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz",
...@@ -3207,6 +3408,18 @@ ...@@ -3207,6 +3408,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": { "node_modules/commander": {
"version": "10.0.1", "version": "10.0.1",
"resolved": "https://registry.npmmirror.com/commander/-/commander-10.0.1.tgz", "resolved": "https://registry.npmmirror.com/commander/-/commander-10.0.1.tgz",
...@@ -3319,6 +3532,12 @@ ...@@ -3319,6 +3532,12 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/dayjs": {
"version": "1.11.18",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.18.tgz",
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==",
"license": "MIT"
},
"node_modules/de-indent": { "node_modules/de-indent": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz",
...@@ -3411,6 +3630,15 @@ ...@@ -3411,6 +3630,15 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/didyoumean": { "node_modules/didyoumean": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz", "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz",
...@@ -3425,6 +3653,20 @@ ...@@ -3425,6 +3653,20 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/eastasianwidth": { "node_modules/eastasianwidth": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
...@@ -3487,6 +3729,32 @@ ...@@ -3487,6 +3729,32 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/element-plus": {
"version": "2.11.3",
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.11.3.tgz",
"integrity": "sha512-769xsjLR4B9Vf9cl5PDXnwTEdmFJvMgAkYtthdJKPhjVjU3hdAwTJ+gXKiO+PUyo2KWFwOYKZd4Ywh6PHfkbJg==",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^3.4.1",
"@element-plus/icons-vue": "^2.3.1",
"@floating-ui/dom": "^1.0.1",
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
"@types/lodash": "^4.14.182",
"@types/lodash-es": "^4.17.6",
"@vueuse/core": "^9.1.0",
"async-validator": "^4.2.5",
"dayjs": "^1.11.13",
"escape-html": "^1.0.3",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"lodash-unified": "^1.0.2",
"memoize-one": "^6.0.0",
"normalize-wheel-es": "^1.2.0"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "9.2.2", "version": "9.2.2",
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz",
...@@ -3516,6 +3784,24 @@ ...@@ -3516,6 +3784,24 @@
"url": "https://github.com/sponsors/antfu" "url": "https://github.com/sponsors/antfu"
} }
}, },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-module-lexer": { "node_modules/es-module-lexer": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz", "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
...@@ -3523,6 +3809,33 @@ ...@@ -3523,6 +3809,33 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.10", "version": "0.25.10",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.10.tgz", "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.10.tgz",
...@@ -3575,6 +3888,12 @@ ...@@ -3575,6 +3888,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/escape-string-regexp": { "node_modules/escape-string-regexp": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
...@@ -4104,6 +4423,26 @@ ...@@ -4104,6 +4423,26 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/foreground-child": { "node_modules/foreground-child": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz",
...@@ -4121,6 +4460,22 @@ ...@@ -4121,6 +4460,22 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fraction.js": { "node_modules/fraction.js": {
"version": "4.3.7", "version": "4.3.7",
"resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz", "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz",
...@@ -4154,7 +4509,6 @@ ...@@ -4154,7 +4509,6 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
...@@ -4170,6 +4524,43 @@ ...@@ -4170,6 +4524,43 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-stream": { "node_modules/get-stream": {
"version": "9.0.1", "version": "9.0.1",
"resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-9.0.1.tgz", "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-9.0.1.tgz",
...@@ -4234,6 +4625,18 @@ ...@@ -4234,6 +4625,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graphemer": { "node_modules/graphemer": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz", "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz",
...@@ -4251,11 +4654,37 @@ ...@@ -4251,11 +4654,37 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
...@@ -4828,6 +5257,29 @@ ...@@ -4828,6 +5257,29 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/lodash-unified": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz",
"integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
"license": "MIT",
"peerDependencies": {
"@types/lodash-es": "*",
"lodash": "*",
"lodash-es": "*"
}
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
...@@ -4861,6 +5313,21 @@ ...@@ -4861,6 +5313,21 @@
"@jridgewell/sourcemap-codec": "^1.5.5" "@jridgewell/sourcemap-codec": "^1.5.5"
} }
}, },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/memorystream": { "node_modules/memorystream": {
"version": "0.3.1", "version": "0.3.1",
"resolved": "https://registry.npmmirror.com/memorystream/-/memorystream-0.3.1.tgz", "resolved": "https://registry.npmmirror.com/memorystream/-/memorystream-0.3.1.tgz",
...@@ -4894,6 +5361,27 @@ ...@@ -4894,6 +5361,27 @@
"node": ">=8.6" "node": ">=8.6"
} }
}, },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz",
...@@ -5030,6 +5518,12 @@ ...@@ -5030,6 +5518,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/normalize-wheel-es": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
"license": "BSD-3-Clause"
},
"node_modules/npm-normalize-package-bin": { "node_modules/npm-normalize-package-bin": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmmirror.com/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", "resolved": "https://registry.npmmirror.com/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz",
...@@ -5727,6 +6221,12 @@ ...@@ -5727,6 +6221,12 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
}, },
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --mode development",
"build": "run-p type-check \"build-only {@}\" --", "build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview", "preview": "vite preview",
"test:unit": "vitest", "test:unit": "vitest",
...@@ -19,6 +19,8 @@ ...@@ -19,6 +19,8 @@
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^7.0.1", "@fortawesome/fontawesome-free": "^7.0.1",
"axios": "^1.12.2",
"element-plus": "^2.11.3",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"vue": "^3.5.18", "vue": "^3.5.18",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
......
...@@ -80,20 +80,20 @@ ...@@ -80,20 +80,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import LoginPage from './components/LoginPage.vue' import LoginPage from './views/LoginPage.vue'
import Sidebar from './components/Sidebar.vue' import Sidebar from './views/Sidebar.vue'
import MobileSidebar from './components/MobileSidebar.vue' import MobileSidebar from './views/MobileSidebar.vue'
import ComposeEmail from './components/ComposeEmail.vue' import ComposeEmail from './views/ComposeEmail.vue'
import ContactManagement from './components/ContactManagement.vue' import ContactManagement from './views/ContactManagement.vue'
import SenderManagement from './components/SenderManagement.vue' import SenderManagement from './views/SenderManagement.vue'
import VariableManagement from './components/VariableManagement.vue' import VariableManagement from './views/VariableManagement.vue'
import EmailManagement from './components/EmailManagement.vue' import EmailManagement from './views/EmailManagement.vue'
import { Contact, Sender, Variable, VariableTemplate, Email } from './types' import type { Contact, Sender, Variable, VariableTemplate, Email } from './types'
// 状态管理 // 状态管理
const isLoginPage = ref(true) const isLoginPage = ref(true)
const isAuthenticated = ref(false) const isAuthenticated = ref(false)
const currentPage = ref('compose') const currentPage = ref<'compose' | 'contacts' | 'senders' | 'variables' | 'emails'>('compose')
const showMobileMenu = ref(false) const showMobileMenu = ref(false)
// 数据存储 // 数据存储
...@@ -127,11 +127,11 @@ const handleLogout = () => { ...@@ -127,11 +127,11 @@ const handleLogout = () => {
} }
const handlePageChange = (page: string) => { const handlePageChange = (page: string) => {
currentPage.value = page currentPage.value = page as 'compose' | 'contacts' | 'senders' | 'variables' | 'emails'
} }
const handleMobilePageChange = (page: string) => { const handleMobilePageChange = (page: string) => {
currentPage.value = page currentPage.value = page as 'compose' | 'contacts' | 'senders' | 'variables' | 'emails'
showMobileMenu.value = false showMobileMenu.value = false
} }
...@@ -155,7 +155,7 @@ const saveEmail = (email: Email) => { ...@@ -155,7 +155,7 @@ const saveEmail = (email: Email) => {
emails.value.push(email) emails.value.push(email)
} }
const reuseEmail = (emailData: any) => { const reuseEmail = (emailData: Email) => {
currentPage.value = 'compose' currentPage.value = 'compose'
// 这里可以传递需要复用的邮件数据到ComposeEmail组件 // 这里可以传递需要复用的邮件数据到ComposeEmail组件
// 实际实现中可以使用状态管理或props // 实际实现中可以使用状态管理或props
...@@ -164,26 +164,7 @@ const reuseEmail = (emailData: any) => { ...@@ -164,26 +164,7 @@ const reuseEmail = (emailData: any) => {
const loadInitialData = () => { const loadInitialData = () => {
// 模拟加载初始数据 // 模拟加载初始数据
// 联系人 // 联系人
contacts.value = [ contacts.value = []
{
id: '1',
name: '张三',
title: '先生',
company: 'ABC公司',
email: 'zhangsan@example.com',
ccEmail: 'zhangsan_cc@example.com',
other: '技术总监',
},
{
id: '2',
name: '李四',
title: '女士',
company: 'XYZ企业',
email: 'lisi@example.com',
ccEmail: '',
other: '市场经理',
},
]
// 发件人 // 发件人
senders.value = [ senders.value = [
...@@ -257,5 +238,8 @@ const loadInitialData = () => { ...@@ -257,5 +238,8 @@ const loadInitialData = () => {
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
// 检查是否已登录(实际项目中应该检查本地存储或令牌) // 检查是否已登录(实际项目中应该检查本地存储或令牌)
if (isAuthenticated.value) {
loadInitialData()
}
}) })
</script> </script>
import request from '@/utils/request'
// 新增联系人
/**
*
* @param data {"companyName":"","name":"","email":"","type":"","appellation":"","other":"","ccEmailList":[]}
* @returns
*/
export const addContact = (data: {
companyName?: string
name?: string
email?: string
type?: string
appellation?: string
other?: string
ccEmailList?: string[]
}) => {
return request.post('/emailContact/add', data)
}
// 编辑联系人
/**
*
* @param data {"contactBizId":1,"companyName":"","name":"","email":"","type":"","appellation":"","other":"","ccEmailList":[]}
* @returns
*/
export const editContact = (data) => {
return request.put('/emailContact/edit', data)
}
// 删除联系人 delete
/**
*
* @param id 联系人id
* @returns
*/
export const deleteContact = (id: number) => {
return request.delete('/emailContact/del?contactBizId=' + id, { params: { contactBizId: id } })
}
// 获取联系人详情
/**
*
* @param id 联系人id
* @returns
*/
export const getContactDetail = (id: number) => {
return request.get('/emailContact/detail', { params: { contactBizId: id } })
}
// 获取联系人列表 post
/**
*
* @param params {
"companyName": "", //公司名称(保险公司等)
"name": "", //联系人姓名
"email": "", //联系人邮箱
"pageNo": 1,
"pageSize": 1,
"sortField": "",
"sortOrder": ""
}
* @returns
*/
export const getContactList = (params: {
companyName?: string
name?: string
email?: string
pageNo?: number
pageSize?: number
sortField?: string
sortOrder?: string
}) => {
return request.post('/emailContact/page', params)
}
// 新增发送配置
/**
*
* @param data {"emailSenderConfigBizId":1,"emailSenderConfigName":"","emailSenderConfigEmail":"","emailSenderConfigType":"","emailSenderConfigAppellation":"","emailSenderConfigOther":"","emailSenderConfigCcEmailList":[]}
* @returns
*/
export const addEmailSenderConfig = (data: {
emailSenderConfigBizId?: number
emailSenderConfigName?: string
emailSenderConfigEmail?: string
emailSenderConfigType?: string
emailSenderConfigAppellation?: string
emailSenderConfigOther?: string
emailSenderConfigCcEmailList?: string[]
}) => {
return request.post('/emailSenderConfig/add', data)
}
// 编辑发送配置
/**
*
* @param data {"emailSenderConfigBizId":1,"emailSenderConfigName":"","emailSenderConfigEmail":"","emailSenderConfigType":"","emailSenderConfigAppellation":"","emailSenderConfigOther":"","emailSenderConfigCcEmailList":[]}
* @returns
*/
export const editEmailSenderConfig = (data: {
emailSenderConfigBizId?: number
emailSenderConfigName?: string
emailSenderConfigEmail?: string
emailSenderConfigType?: string
emailSenderConfigAppellation?: string
emailSenderConfigOther?: string
emailSenderConfigCcEmailList?: string[]
}) => {
return request.put('/emailSenderConfig/edit', data)
}
// 删除发送配置
/**
*
* @param id 发送配置id
* @returns
*/
export const deleteEmailSenderConfig = (id: number) => {
return request.delete('/emailSenderConfig/delete', { params: { emailSenderConfigBizId: id } })
}
// 获取发送配置详情
/**
*
* @param id 发送配置id
* @returns
*/
export const getEmailSenderConfigDetail = (id: number) => {
return request.get('/emailSenderConfig/detail', { params: { emailSenderConfigBizId: id } })
}
// 获取发送配置列表
/**
*
* @param params {
"emailSenderConfigName": "", //发送配置名称
"emailSenderConfigEmail": "", //发送配置邮箱
"pageNo": 1,
"pageSize": 1,
"sortField": "",
"sortOrder": ""
}
* @returns
*/
export const getEmailSenderConfigList = (params: {
emailSenderConfigName?: string
emailSenderConfigEmail?: string
pageNo?: number
pageSize?: number
sortField?: string
sortOrder?: string
}) => {
return request.post('/emailSenderConfig/page', params)
}
// 分页查询变量
// 接口地址:/emailVariable/page
// 请求参数:{"variableNameCn":"","variableNameEn":"","pageNo":1,"pageSize":1,"sortField":"","sortOrder":""}
/**
*
* @param params {
"variableNameCn": "", //变量名称(中文)
"variableNameEn": "", //变量名称(英文)
"pageNo": 1,
"pageSize": 1,
"sortField": "",
"sortOrder": ""
}
* @returns
*/
export const getEmailVariableList = (params: {
variableNameCn?: string
variableNameEn?: string
pageNo?: number
pageSize?: number
sortField?: string
sortOrder?: string
}) => {
return request.post('/emailVariable/page', params)
}
// 新增变量
// 接口地址:/emailVariable/add
/**
*
* @param data {"variableNameCn": "", //变量字段名称中文名
"variableNameEn": "", //变量字段名称英文名
"description": "" //变量描述
}
* @returns
*/
export const addEmailVariable = (data: {
variableNameCn?: string
variableNameEn?: string
description?: string
}) => {
return request.post('/emailVariable/add', data)
}
// 编辑变量
// 接口地址:/emailVariable/edit
/**
*
* @param data {"id": 1, //变量表主键ID
"variableBizId": "", //变量唯一业务ID
"variableNameCn": "", //变量字段名称中文名
"variableNameEn": "", //变量字段名称英文名
"description": "" //变量描述
}
* @returns
*/
export const editEmailVariable = (data: {
id?: number
variableBizId?: string
variableNameCn?: string
variableNameEn?: string
description?: string
}) => {
return request.put('/emailVariable/edit', data)
}
// 删除变量
// 接口地址:/emailVariable/del?variableBizId=
// 请求参数:
/**
*
* @param id 变量id
* @returns
*/
export const deleteEmailVariable = (id: string) => {
return request.delete('/emailVariable/del?variableBizId=' + id)
}
/**
* 新增变量分组
* @param data {"variableGroupBizId": "", //变量分组唯一业务ID
"variableGroupName": "", //变量分组名称
"description": "" //变量分组描述
}
* @returns
*/
export const addEmailVariableGroup = (data: {
variableBizIdList?: string[]
groupName?: string
description?: string
}) => {
return request.post('/emailVariableGroup/add', data)
}
/**
* 编辑变量分组
* @param data {"variableGroupBizId": "", //变量分组唯一业务ID
"variableGroupName": "", //变量分组名称
"description": "" //变量分组描述
}
* @returns
*/
export const editEmailVariableGroup = (data: {
variableGroupBizId?: string
groupName?: string
description?: string
variableBizIdList?: string[]
}) => {
return request.put('/emailVariableGroup/edit', data)
}
/**
* 删除变量分组
* @param id 变量分组id
* @returns
*/
export const deleteEmailVariableGroup = (id: string) => {
return request.delete('/emailVariableGroup/del?variableGroupBizId=' + id)
}
/**
* 列表查询变量分组
* @param params {
"variableGroupName": "", //变量分组名称
"pageNo": 1,
"pageSize": 1,
"sortField": "",
"sortOrder": ""
}
* @returns
*/
export const getEmailVariableGroupList = (params: {
groupName?: string
pageNo?: number
pageSize?: number
sortField?: string
sortOrder?: string
}) => {
return request.post('/emailVariableGroup/page', params)
}
<template>
<el-dialog
v-model="dialogVisible"
:title="title"
:width="width"
:top="top"
:modal="modal"
:close-on-click-modal="closeOnClickModal"
:close-on-press-escape="closeOnPressEscape"
:show-close="showClose"
:destroy-on-close="destroyOnClose"
:custom-class="customClass"
@close="handleClose"
>
<!-- 弹窗头部插槽 -->
<template #header v-if="$slots.header">
<slot name="header"></slot>
</template>
<!-- 弹窗内容 -->
<div class="modal-content">
<!-- 图标区域 -->
<div
v-if="type && showIcon"
class="icon-container mr-4 flex-shrink-0"
:class="iconContainerClass"
>
<component :is="getIconComponent" class="w-6 h-6" />
</div>
<!-- 内容区域 -->
<div class="content-container flex-1">
<!-- 默认消息内容 -->
<template v-if="!$slots.default">
<p class="text-gray-800 text-sm leading-6 mb-0">
{{ message }}
</p>
<p v-if="subMessage" class="text-gray-500 text-xs leading-5 mt-2">
{{ subMessage }}
</p>
</template>
<!-- 自定义内容插槽 -->
<slot></slot>
</div>
</div>
<!-- 弹窗底部 -->
<template #footer>
<slot name="footer">
<div class="flex gap-3 justify-end">
<el-button
v-if="showCancelButton"
@click="handleCancel"
size="default"
:loading="cancelLoading"
class="px-4 py-2"
>
{{ cancelText }}
</el-button>
<el-button
@click="handleConfirm"
:type="getButtonType"
size="default"
:loading="confirmLoading"
class="px-4 py-2"
>
{{ confirmText }}
</el-button>
</div>
</slot>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { defineProps, defineEmits, computed, watch, ref, onUnmounted } from 'vue'
import { Check, Warning, CircleClose, InfoFilled, QuestionFilled } from '@element-plus/icons-vue'
// 弹窗类型
type ModalType = 'success' | 'warning' | 'error' | 'info' | 'confirm' | ''
// 定义属性
const props = defineProps({
/** 控制弹窗显示/隐藏 */
visible: {
type: Boolean,
default: false,
},
/** 弹窗类型 */
type: {
type: String as () => ModalType,
default: '',
},
/** 弹窗标题 */
title: {
type: String,
default: '',
},
/** 主要消息内容 */
message: {
type: String,
default: '',
},
/** 次要消息内容 */
subMessage: {
type: String,
default: '',
},
/** 弹窗宽度 */
width: {
type: String,
default: '500px',
},
/** 弹窗距离顶部的距离 */
top: {
type: String,
default: '15vh',
},
/** 是否显示遮罩层 */
modal: {
type: Boolean,
default: true,
},
/** 点击遮罩层是否关闭弹窗 */
closeOnClickModal: {
type: Boolean,
default: false,
},
/** 按ESC键是否关闭弹窗 */
closeOnPressEscape: {
type: Boolean,
default: true,
},
/** 是否显示关闭按钮 */
showClose: {
type: Boolean,
default: true,
},
/** 是否在关闭弹窗时销毁内容 */
destroyOnClose: {
type: Boolean,
default: true,
},
/** 是否显示取消按钮 */
showCancelButton: {
type: Boolean,
default: false,
},
/** 确认按钮文本 */
confirmText: {
type: String,
default: '确定',
},
/** 取消按钮文本 */
cancelText: {
type: String,
default: '取消',
},
/** 自动关闭时间(毫秒),0表示不自动关闭 */
autoClose: {
type: Number,
default: 0,
},
/** 自定义CSS类 */
customClass: {
type: String,
default: '',
},
/** 是否显示图标 */
showIcon: {
type: Boolean,
default: true,
},
/** 确认按钮加载状态 */
confirmLoading: {
type: Boolean,
default: false,
},
/** 取消按钮加载状态 */
cancelLoading: {
type: Boolean,
default: false,
},
/** 是否可拖拽 */
draggable: {
type: Boolean,
default: false,
},
})
// 定义事件
const emit = defineEmits([
'confirm',
'cancel',
'close',
'update:visible',
'update:confirmLoading',
'update:cancelLoading',
])
// 内部状态管理
const dialogVisible = ref(props.visible)
const autoCloseTimer = ref<NodeJS.Timeout | null>(null)
// 监听visible变化
watch(
() => props.visible,
(newVal) => {
dialogVisible.value = newVal
},
)
// 监听dialogVisible变化
watch(
() => dialogVisible.value,
(newVal) => {
if (newVal) {
// 处理自动关闭
if (props.autoClose > 0) {
clearAutoCloseTimer()
autoCloseTimer.value = setTimeout(() => {
handleClose()
}, props.autoClose)
}
} else {
// 触发update:visible事件
emit('update:visible', false)
clearAutoCloseTimer()
}
},
)
// 清除自动关闭定时器
const clearAutoCloseTimer = () => {
if (autoCloseTimer.value) {
clearTimeout(autoCloseTimer.value)
autoCloseTimer.value = null
}
}
// 根据类型获取按钮样式
const getButtonType = computed(() => {
switch (props.type) {
case 'success':
return 'success'
case 'warning':
return 'warning'
case 'error':
return 'danger'
case 'info':
return 'info'
case 'confirm':
return 'primary'
default:
return 'primary'
}
})
// 获取图标组件
const getIconComponent = computed<Component>(() => {
switch (props.type) {
case 'success':
return Check
case 'warning':
return Warning
case 'error':
return CircleClose
case 'info':
return InfoFilled
case 'confirm':
return QuestionFilled
default:
return InfoFilled
}
})
// 图标容器样式
const iconContainerClass = computed(() => {
const base = 'rounded-full p-2'
switch (props.type) {
case 'success':
return `${base} bg-green-100 text-green-600`
case 'warning':
return `${base} bg-yellow-100 text-yellow-600`
case 'error':
return `${base} bg-red-100 text-red-600`
case 'info':
return `${base} bg-blue-100 text-blue-600`
case 'confirm':
return `${base} bg-gray-100 text-gray-600`
default:
return `${base} bg-gray-100 text-gray-600`
}
})
// 处理关闭事件
const handleClose = () => {
dialogVisible.value = false
emit('close')
}
// 处理确认事件
const handleConfirm = () => {
emit('confirm')
// 不自动关闭,由父组件控制
if (!props.confirmLoading) {
handleClose()
}
}
// 处理取消事件
const handleCancel = () => {
emit('cancel')
handleClose()
}
// 组件卸载时清理定时器
onUnmounted(() => {
clearAutoCloseTimer()
})
</script>
<style scoped>
.modal-content {
@apply flex items-start;
}
.icon-container {
@apply flex items-center justify-center;
}
.content-container {
@apply min-h-0;
}
/* 适配Element Plus的样式 */
:deep(.el-dialog__body) {
@apply px-6 py-4;
}
:deep(.el-dialog__header) {
@apply border-b border-gray-200 pb-4 mb-0;
}
:deep(.el-dialog__footer) {
@apply border-t border-gray-200 pt-4 mt-0;
}
</style>
<template>
<div>
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">添加联系人</h3>
<button
v-if="editingContactId"
@click="resetForm"
class="text-gray-500 hover:text-gray-700"
>
<i class="fas fa-times"></i> 取消编辑
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div>
<label class="block text-gray-700 mb-1 text-sm">姓名 *</label>
<input
v-model="formData.name"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label class="block text-gray-700 mb-1 text-sm">称谓</label>
<input
v-model="formData.title"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="先生/女士/教授等"
/>
</div>
<div>
<label class="block text-gray-700 mb-1 text-sm">公司</label>
<input
v-model="formData.company"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label class="block text-gray-700 mb-1 text-sm">邮箱 *</label>
<input
v-model="formData.email"
type="email"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label class="block text-gray-700 mb-1 text-sm">关联抄送邮箱</label>
<input
v-model="formData.ccEmail"
type="email"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="当此联系人作为收件人时自动抄送的邮箱"
/>
</div>
<div>
<label class="block text-gray-700 mb-1 text-sm">其他信息</label>
<input
v-model="formData.other"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
<div class="mt-4 flex justify-end">
<button
@click="saveContact"
class="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-700 transition-colors"
:disabled="!formData.name || !formData.email"
>
{{ editingContactId ? '更新联系人' : '添加联系人' }}
</button>
</div>
</div>
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<div class="p-6 border-b border-gray-200">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<h3 class="text-lg font-semibold">联系人列表</h3>
<div class="w-full sm:w-auto">
<div class="relative">
<input
v-model="searchTerm"
type="text"
class="w-full px-3 py-2 pl-10 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="搜索联系人..."
/>
<i
class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
></i>
</div>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
姓名
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
称谓
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
公司
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
邮箱
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
抄送邮箱
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
其他信息
</th>
<th
class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"
>
操作
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="contact in filteredContacts" :key="contact.id">
<td class="px-6 py-4 whitespace-nowrap">{{ contact.name }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ contact.title || '-' }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ contact.company || '-' }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ contact.email }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ contact.ccEmail || '-' }}</td>
<td class="px-6 py-4">{{ contact.other || '-' }}</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
@click="editContact(contact)"
class="text-blue-600 hover:text-blue-900 mr-3"
>
编辑
</button>
<button @click="deleteContact(contact.id)" class="text-red-600 hover:text-red-900">
删除
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="filteredContacts.length === 0" class="p-8 text-center text-gray-500">
<i class="fas fa-address-book text-4xl mb-3 opacity-30"></i>
<p>暂无联系人,请添加联系人</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, defineProps, defineEmits } from 'vue'
import { Contact } from '../types'
const props = defineProps({
contacts: {
type: Array as () => Contact[],
required: true,
},
})
const emits = defineEmits(['update-contacts'])
// 状态
const contacts = ref<Contact[]>([...props.contacts])
const searchTerm = ref('')
const editingContactId = ref('')
const formData = ref<Partial<Contact>>({
name: '',
title: '',
company: '',
email: '',
ccEmail: '',
other: '',
})
// 计算属性
const filteredContacts = computed(() => {
return contacts.value.filter(
(contact) =>
contact.name.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
contact.email.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
contact.company.toLowerCase().includes(searchTerm.value.toLowerCase()),
)
})
// 方法
const resetForm = () => {
editingContactId.value = ''
formData.value = {
name: '',
title: '',
company: '',
email: '',
ccEmail: '',
other: '',
}
}
const saveContact = () => {
if (!formData.value.name || !formData.value.email) return
if (editingContactId.value) {
// 更新现有联系人
const index = contacts.value.findIndex((c) => c.id === editingContactId.value)
if (index > -1) {
contacts.value[index] = {
...contacts.value[index],
...formData.value,
} as Contact
emits('update-contacts', [...contacts.value])
alert('联系人更新成功')
}
} else {
// 添加新联系人
const newContact: Contact = {
id: Date.now().toString(),
name: formData.value.name || '',
title: formData.value.title || '',
company: formData.value.company || '',
email: formData.value.email || '',
ccEmail: formData.value.ccEmail || '',
other: formData.value.other || '',
}
contacts.value.push(newContact)
emits('update-contacts', [...contacts.value])
alert('联系人添加成功')
}
resetForm()
}
const editContact = (contact: Contact) => {
editingContactId.value = contact.id
formData.value = { ...contact }
}
const deleteContact = (id: string) => {
if (confirm('确定要删除这个联系人吗?')) {
contacts.value = contacts.value.filter((contact) => contact.id !== id)
emits('update-contacts', [...contacts.value])
}
}
</script>
/* 导入Font Awesome */
/* @import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css'); */
/* 导入Tailwind CSS */ /* 导入Tailwind CSS */
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* 导入Font Awesome */
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css');
/* 自定义样式 */ /* 自定义样式 */
#app { #app {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
......
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
...@@ -8,6 +10,7 @@ import router from './router' ...@@ -8,6 +10,7 @@ import router from './router'
import './index.css' import './index.css'
const app = createApp(App) const app = createApp(App)
app.use(ElementPlus)
app.use(createPinia()) app.use(createPinia())
app.use(router) app.use(router)
......
...@@ -2,11 +2,12 @@ ...@@ -2,11 +2,12 @@
export interface Contact { export interface Contact {
id: string id: string
name: string name: string
title: string type: string
company: string companyName: string
email: string email: string
ccEmail: string ccEmailList: string[]
other: string other: string
appellation: string
} }
// 发件人类型 // 发件人类型
...@@ -20,18 +21,19 @@ export interface Sender { ...@@ -20,18 +21,19 @@ export interface Sender {
// 变量类型 // 变量类型
export interface Variable { export interface Variable {
id: string id?: string
name: string variableBizId?: string
key: string variableNameCn?: string
description: string variableNameEn?: string
description?: string
} }
// 变量模板类型 // 变量模板类型
export interface VariableTemplate { export interface VariableTemplate {
id: string id: string
name: string groupName?: string
description: string description?: string
variableIds: string[] variableBizIdList?: string[]
} }
// 邮件类型 // 邮件类型
...@@ -63,3 +65,12 @@ export interface ForgotPasswordForm { ...@@ -63,3 +65,12 @@ export interface ForgotPasswordForm {
newPassword: string newPassword: string
confirmPassword: string confirmPassword: string
} }
// 导入记录类型
export interface ImportRecord {
id: string
to: string
cc: string
createdAt: string
updatedAt: string
}
import axios, { AxiosError } from 'axios'
// 创建axios实例
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
// Authorization: 'Bearer ' + localStorage.getItem('authToken'),
Authorization:
'Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyXzEwMDEiLCJyb2xlcyI6W10sImlhdCI6MTc1ODY5OTA3NywiZXhwIjoxNzU4Nzg1NDc3fQ.LR2fGy0aO6EHsHe9Que8rzCaJ0TSAB9KtJndYMSYvvKOSeNvGawCmjE8kgDeRmyFFOFJ2kt0sk-fGaExgzQHSw',
},
})
// 请求拦截器 - 添加Authorization头
request.interceptors.request.use(
(config) => {
// 从本地存储获取token
const token = localStorage.getItem('authToken')
// 如果token存在,添加到请求头
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error: AxiosError) => {
// 处理请求错误
return Promise.reject(error)
},
)
// 响应拦截器 - 处理常见错误
request.interceptors.response.use(
(response) => {
// 直接返回响应数据
return response.data
},
(error: AxiosError) => {
// 处理401未授权错误
if (error.response && error.response.status === 401) {
// 清除无效token
localStorage.removeItem('authToken')
// 如果不是登录页面,跳转到登录页
if (!window.location.pathname.includes('/login')) {
// 保存当前URL,登录后可跳转回来
localStorage.setItem('redirectPath', window.location.pathname)
window.location.href = '/login'
}
}
return Promise.reject(error)
},
)
export default request
...@@ -11,27 +11,53 @@ ...@@ -11,27 +11,53 @@
</option> </option>
</select> </select>
</div> </div>
<div class="mb-4">
<label class="block text-gray-700 mb-2 font-medium">变量模板</label>
<div class="flex flex-col sm:flex-row gap-2">
<select
v-model="selectedVariableTemplate"
class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
@change="applyVariableTemplate"
>
<option value="">-- 选择模板 --</option>
<option v-for="template in variableTemplates" :key="template.id" :value="template">
{{ template.name }}
</option>
</select>
<!-- 当选择模版有值时,显示导入数据按钮 -->
<button
@click="showImportContacts = true"
class="bg-blue-50 hover:bg-blue-100 text-blue-600 px-4 py-2 rounded-md border border-blue-200 transition-colors flex items-center"
v-if="selectedVariableTemplate"
>
<i class="fas fa-address-book mr-1"></i> 导入数据
</button>
</div>
</div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-gray-700 mb-2 font-medium">收件人</label> <label class="block text-gray-700 mb-2 font-medium">收件人</label>
<div class="flex flex-col sm:flex-row gap-2"> <div class="flex flex-col sm:flex-row gap-2">
<!-- 多个邮箱用tag的样式展示 -->
<input <input
v-model="emailForm.to" v-model="emailForm.to"
type="text" type="text"
class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="输入收件人邮箱,多个邮箱用逗号分隔" placeholder="输入收件人邮箱,多个邮箱用逗号分隔"
/> />
<!-- 当选择模版有值时,不显示选择联系人按钮 -->
<button <button
@click="showContactSelector = true" @click="showContactSelector = true"
class="bg-blue-50 hover:bg-blue-100 text-blue-600 px-4 py-2 rounded-md border border-blue-200 transition-colors flex items-center" class="bg-blue-50 hover:bg-blue-100 text-blue-600 px-4 py-2 rounded-md border border-blue-200 transition-colors flex items-center"
v-if="!selectedVariableTemplate"
> >
<i class="fas fa-address-book mr-1"></i> 选择联系人 <i class="fas fa-address-book mr-1"></i> 选择联系人
</button> </button>
<button <button
@click="showImportContacts = true" @click="showImportRecordManager = true"
class="bg-blue-50 hover:bg-blue-100 text-blue-600 px-4 py-2 rounded-md border border-blue-200 transition-colors flex items-center" class="bg-green-50 hover:bg-green-100 text-green-600 px-4 py-2 rounded-md border border-green-200 transition-colors flex items-center"
v-if="importRecords.length > 0"
> >
<i class="fas fa-upload mr-1"></i> 导入 <i class="fas fa-cog mr-1"></i> 编辑数据
</button> </button>
</div> </div>
</div> </div>
...@@ -55,21 +81,6 @@ ...@@ -55,21 +81,6 @@
placeholder="邮件主题" placeholder="邮件主题"
/> />
</div> </div>
<div class="mb-4">
<label class="block text-gray-700 mb-2 font-medium">变量模板</label>
<select
v-model="selectedVariableTemplate"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
@change="applyVariableTemplate"
>
<option value="">-- 选择模板 --</option>
<option v-for="template in variableTemplates" :key="template.id" :value="template">
{{ template.name }}
</option>
</select>
</div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-gray-700 mb-2 font-medium">正文</label> <label class="block text-gray-700 mb-2 font-medium">正文</label>
<div class="border border-gray-300 rounded-md overflow-hidden"> <div class="border border-gray-300 rounded-md overflow-hidden">
...@@ -78,7 +89,7 @@ ...@@ -78,7 +89,7 @@
@click="showVariableSelector = true" @click="showVariableSelector = true"
class="text-sm bg-blue-50 hover:bg-blue-100 text-blue-600 px-3 py-1 rounded border border-blue-200 transition-colors" class="text-sm bg-blue-50 hover:bg-blue-100 text-blue-600 px-3 py-1 rounded border border-blue-200 transition-colors"
> >
<i class="fas fa-variable mr-1"></i> 插入变量 <i class="fas fa-variable mr-1"></i> 插入字段
</button> </button>
</div> </div>
<textarea <textarea
...@@ -156,7 +167,44 @@ ...@@ -156,7 +167,44 @@
发送 发送
</button> </button>
</div> </div>
<!-- 导入记录管理弹窗 -->
<ImportRecordManager
v-if="showImportRecordManager"
:records="importRecords"
@update-record="updateImportRecord"
@delete-record="deleteImportRecord"
@close="showImportRecordManager = false"
/>
<!-- 导入数据弹窗 -->
<ImportDialog
v-model:visible="showImportContacts"
title="导入数据"
accept=".csv,.txt,.xlsx"
@file-selected="handleImportContacts"
/>
<div
v-if="showImportContacts"
class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4"
>
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md">
<h3 class="text-lg font-semibold mb-4">导入联系人</h3>
<input
type="file"
accept=".csv,.txt,.xlsx"
@change="handleImportContacts"
class="w-full px-3 py-2 border border-gray-300 rounded-md mb-4"
/>
<div class="flex justify-end gap-3">
<button
@click="showImportContacts = false"
class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50"
>
取消
</button>
</div>
</div>
</div>
<!-- 联系人选择弹窗 --> <!-- 联系人选择弹窗 -->
<ContactSelector <ContactSelector
v-if="showContactSelector" v-if="showContactSelector"
...@@ -186,11 +234,21 @@ ...@@ -186,11 +234,21 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, defineProps, defineEmits, computed } from 'vue' import { ref, watch } from 'vue'
import ContactSelector from './ContactSelector.vue' import ContactSelector from './ContactSelector.vue'
import VariableSelector from './VariableSelector.vue' import VariableSelector from './VariableSelector.vue'
import EmailPreview from './EmailPreview.vue' import EmailPreview from './EmailPreview.vue'
import { Sender, Contact, Variable, VariableTemplate, Email, EmailForm } from '../types' import ImportRecordManager from './ImportRecordManager.vue'
import ImportDialog from './ImportDialog.vue'
import {
Sender,
Contact,
Variable,
VariableTemplate,
Email,
EmailForm,
ImportRecord,
} from '../types'
const props = defineProps({ const props = defineProps({
senders: { senders: {
...@@ -215,7 +273,7 @@ const props = defineProps({ ...@@ -215,7 +273,7 @@ const props = defineProps({
}, },
}) })
const emits = defineEmits(['save-email']) const emits = defineEmits(['save-email', 'save-import-record'])
// 状态 // 状态
const currentSender = ref<Sender | null>(props.senders.length > 0 ? props.senders[0] : null) const currentSender = ref<Sender | null>(props.senders.length > 0 ? props.senders[0] : null)
...@@ -233,6 +291,21 @@ const showContactSelector = ref(false) ...@@ -233,6 +291,21 @@ const showContactSelector = ref(false)
const showVariableSelector = ref(false) const showVariableSelector = ref(false)
const showPreview = ref(false) const showPreview = ref(false)
const showImportContacts = ref(false) const showImportContacts = ref(false)
const showImportRecordManager = ref(false)
const importRecords = ref<ImportRecord[]>([])
// 监听收件人变化,自动匹配抄送人
watch(
() => emailForm.value.to,
(newTo) => {
if (newTo) {
const matchedRecord = importRecords.value.find((record) => record.to === newTo)
if (matchedRecord && matchedRecord.cc) {
emailForm.value.cc = matchedRecord.cc
}
}
},
)
// 计算属性 // 计算属性
const variablePrefix = '{{' const variablePrefix = '{{'
...@@ -242,7 +315,6 @@ const handleFileUpload = (e: Event) => { ...@@ -242,7 +315,6 @@ const handleFileUpload = (e: Event) => {
const input = e.target as HTMLInputElement const input = e.target as HTMLInputElement
if (input.files) { if (input.files) {
attachments.value = [...attachments.value, ...Array.from(input.files)] attachments.value = [...attachments.value, ...Array.from(input.files)]
// 重置输入,允许重复选择同一文件
input.value = '' input.value = ''
} }
} }
...@@ -253,16 +325,11 @@ const removeAttachment = (index: number) => { ...@@ -253,16 +325,11 @@ const removeAttachment = (index: number) => {
const applyVariableTemplate = () => { const applyVariableTemplate = () => {
if (!selectedVariableTemplate.value) return if (!selectedVariableTemplate.value) return
// 根据模板预设内容
const variableKeys = selectedVariableTemplate.value.variableIds.map((id) => { const variableKeys = selectedVariableTemplate.value.variableIds.map((id) => {
const variable = props.variables.find((v) => v.id === id) const variable = props.variables.find((v) => v.id === id)
return variable ? variable.key : '' return variable ? variable.key : ''
}) })
const variablesText = variableKeys.map((key) => `${variablePrefix}${key}}`).join(', ') const variablesText = variableKeys.map((key) => `${variablePrefix}${key}}`).join(', ')
// 在内容前添加模板提示
emailForm.value.content = `【使用了模板变量:${variablesText}】\n\n${emailForm.value.content}` emailForm.value.content = `【使用了模板变量:${variablesText}】\n\n${emailForm.value.content}`
} }
...@@ -275,15 +342,66 @@ const confirmContactSelection = (selected: { to: string; cc: string }) => { ...@@ -275,15 +342,66 @@ const confirmContactSelection = (selected: { to: string; cc: string }) => {
emailForm.value.to = selected.to emailForm.value.to = selected.to
emailForm.value.cc = selected.cc emailForm.value.cc = selected.cc
showContactSelector.value = false showContactSelector.value = false
// 保存导入记录
saveImportRecord(selected.to, selected.cc)
}
// 保存导入记录
const saveImportRecord = (to: string, cc: string) => {
const existingRecord = importRecords.value.find((record) => record.to === to)
if (existingRecord) {
// 更新现有记录
existingRecord.cc = cc
existingRecord.updatedAt = new Date().toISOString()
} else {
// 添加新记录
const newRecord: ImportRecord = {
id: Date.now().toString(),
to,
cc,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
importRecords.value.push(newRecord)
}
emits('save-import-record', importRecords.value)
}
// 更新导入记录
const updateImportRecord = (updatedRecord: ImportRecord) => {
const index = importRecords.value.findIndex((record) => record.id === updatedRecord.id)
if (index !== -1) {
importRecords.value[index] = { ...updatedRecord }
emits('save-import-record', importRecords.value)
}
} }
// 删除导入记录
const deleteImportRecord = (id: string) => {
importRecords.value = importRecords.value.filter((record) => record.id !== id)
emits('save-import-record', importRecords.value)
}
// 修改handleImportContacts方法
const handleImportContacts = (event: { file: File; content: string }) => {
const { content } = event
// 解析CSV或文本文件,这里简化处理
const lines = content.split('\n')
lines.forEach((line) => {
const [to, cc] = line.split(',')
if (to && to.includes('@')) {
saveImportRecord(to.trim(), cc ? cc.trim() : '')
}
})
}
const saveAsDraft = () => { const saveAsDraft = () => {
if (!currentSender.value) { if (!currentSender.value) {
alert('请添加并选择发件人') alert('请添加并选择发件人')
return return
} }
// 保存为草稿
const draft: Email = { const draft: Email = {
id: Date.now().toString(), id: Date.now().toString(),
sender: currentSender.value.email, sender: currentSender.value.email,
...@@ -295,13 +413,11 @@ const saveAsDraft = () => { ...@@ -295,13 +413,11 @@ const saveAsDraft = () => {
status: 'draft', status: 'draft',
attachments: attachments.value.map((file) => ({ name: file.name })), attachments: attachments.value.map((file) => ({ name: file.name })),
} }
emits('save-email', draft) emits('save-email', draft)
alert('草稿已保存') alert('草稿已保存')
} }
const sendEmail = () => { const sendEmail = () => {
// 显示预览
showPreview.value = true showPreview.value = true
} }
...@@ -310,19 +426,13 @@ const confirmSendEmail = () => { ...@@ -310,19 +426,13 @@ const confirmSendEmail = () => {
alert('请添加并选择发件人') alert('请添加并选择发件人')
return return
} }
if (!emailForm.value.to) { if (!emailForm.value.to) {
alert('请填写收件人') alert('请填写收件人')
return return
} }
if (!emailForm.value.subject && !confirm('确定不填写邮件主题吗?')) {
if (!emailForm.value.subject) { return
if (!confirm('确定不填写邮件主题吗?')) {
return
}
} }
// 创建邮件记录
const email: Email = { const email: Email = {
id: Date.now().toString(), id: Date.now().toString(),
sender: currentSender.value.email, sender: currentSender.value.email,
...@@ -337,21 +447,10 @@ const confirmSendEmail = () => { ...@@ -337,21 +447,10 @@ const confirmSendEmail = () => {
status: emailForm.value.scheduleSend ? 'scheduled' : 'sent', status: emailForm.value.scheduleSend ? 'scheduled' : 'sent',
attachments: attachments.value.map((file) => ({ name: file.name })), attachments: attachments.value.map((file) => ({ name: file.name })),
} }
emits('save-email', email) emits('save-email', email)
emailForm.value = { to: '', cc: '', subject: '', content: '', scheduleSend: false, sendTime: '' }
// 重置表单
emailForm.value = {
to: '',
cc: '',
subject: '',
content: '',
scheduleSend: false,
sendTime: '',
}
attachments.value = [] attachments.value = []
selectedVariableTemplate.value = null selectedVariableTemplate.value = null
showPreview.value = false showPreview.value = false
alert(emailForm.value.scheduleSend ? '邮件已安排定时发送' : '邮件发送成功') alert(emailForm.value.scheduleSend ? '邮件已安排定时发送' : '邮件发送成功')
} }
......
<template>
<!-- 模板内容与之前保持一致 -->
<div class="p-4 md:p-6">
<div class="max-w-7xl mx-auto">
<!-- 页面标题和操作按钮 -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6 gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-800">联系人管理</h1>
<p class="text-gray-500 mt-1">管理您的所有联系人信息</p>
</div>
<div class="flex gap-3">
<button @click="showImportModal = true" class="btn-outline flex items-center">
<i class="fas fa-upload mr-2"></i> 批量导入
</button>
<button @click="openAddContactModal()" class="btn-primary flex items-center">
<i class="fas fa-plus mr-2"></i> 新增联系人
</button>
</div>
</div>
<!-- 搜索和筛选区域 -->
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
<div class="flex flex-col md:flex-row gap-4">
<div class="relative flex-1">
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input
v-model="searchQuery"
type="text"
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg"
placeholder="搜索联系人姓名、公司或邮箱..."
@keyup.enter="searchContacts"
/>
</div>
<div class="flex gap-3 w-full md:w-auto">
<select
v-model="sortBy"
class="w-full md:w-auto px-4 py-2 border border-gray-300 rounded-lg"
@change="searchContacts"
>
<option value="name">按姓名排序</option>
<option value="company">按公司排序</option>
<option value="addedAt">按添加时间排序</option>
</select>
<button @click="resetSearch" class="btn-outline px-4">重置</button>
</div>
</div>
</div>
<!-- 联系人列表 -->
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="bg-gray-50 border-b border-gray-200">
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
姓名
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
称谓
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
公司
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
收件人邮箱
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
抄送人邮箱
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
其他信息
</th>
<th
class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"
>
操作
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr
v-for="contact in filteredContacts"
:key="contact.id"
class="hover:bg-gray-50 transition-colors"
>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div
class="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 mr-3"
>
<span class="text-sm font-medium">{{ contact.name.charAt(0) }}</span>
</div>
<div>
<div class="text-sm font-medium text-gray-900">{{ contact.name }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-500">{{ contact.title || '-' }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-500">{{ contact.company || '-' }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-500">{{ contact.toEmail }}</div>
</td>
<td class="px-6 py-4">
<div class="flex flex-wrap gap-1">
<span
v-for="(email, index) in contact.ccEmails"
:key="index"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
>
{{ email }}
</span>
<span v-if="contact.ccEmails.length === 0" class="text-sm text-gray-500"
>-</span
>
</div>
</td>
<td class="px-6 py-4">
<div class="text-sm text-gray-500 line-clamp-2">
{{ contact.otherInfo || '-' }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
@click="editContact(contact)"
class="text-blue-600 hover:text-blue-900 mr-4"
title="编辑"
>
<i class="fas fa-edit"></i>
</button>
<button
@click="deleteContact(contact.id)"
class="text-red-600 hover:text-red-900"
title="删除"
>
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 空状态、加载状态和分页组件与之前保持一致 -->
</div>
</div>
<!-- 模态框组件与之前保持一致 -->
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
// 引入我们创建的api拦截器
import { editContact } from '@/api/api'
// 联系人数据结构
interface Contact {
id: number
name: string
title?: string
company?: string
toEmail: string
ccEmailList: string[]
other?: string
appellation?: string
addedAt: string
}
// 导入错误数据结构
interface ImportError {
row: number
message: string
}
// 导入结果数据结构
interface ImportResult {
success: boolean
message: string
stats?: {
success: number
skipped: number
failed: number
}
errors?: ImportError[]
}
// 页面状态
const contacts = ref<Contact[]>([])
const searchQuery = ref('')
const sortBy = ref('name')
const currentPage = ref(1)
const pageSize = ref(10)
const totalContacts = ref(0)
const isLoading = ref(false)
// 模态框状态
const showContactModal = ref(false)
const isEditing = ref(false)
const showImportModal = ref(false)
const showImportResultModal = ref(false)
// 表单数据
const form = ref<Partial<Contact>>({
id: 0,
name: '',
title: '',
company: '',
toEmail: '',
ccEmails: [],
otherInfo: '',
})
const newCcEmail = ref('')
// 表单错误
const errors = ref({
name: '',
toEmail: '',
})
// 导入相关状态
const importFile = ref<File | null>(null)
const isImporting = ref(false)
const isSubmitting = ref(false)
const importResult = ref<ImportResult>({
success: false,
message: '',
})
// 提示信息状态
const showSuccessToast = ref(false)
const showErrorToast = ref(false)
const successMessage = ref('')
const errorMessage = ref('')
// 初始化页面
onMounted(() => {
fetchContacts()
})
// 过滤联系人
const filteredContacts = computed<Contact[]>(() => {
return contacts.value
})
// 总页数
const totalPages = computed(() => {
return Math.ceil(totalContacts.value / pageSize.value)
})
// 获取联系人列表 - 使用api拦截器
const fetchContacts = async () => {
try {
isLoading.value = true
// 使用我们的api实例,会自动添加Authorization头
const data = await api.get(
`/contacts?page=${currentPage.value}&size=${pageSize.value}&search=${searchQuery.value}&sort=${sortBy.value}`,
)
contacts.value = data.items
totalContacts.value = data.total
} catch (error) {
console.error('获取联系人失败:', error)
showError('获取联系人失败,请稍后重试')
} finally {
isLoading.value = false
}
}
// 搜索联系人
const searchContacts = () => {
currentPage.value = 1
fetchContacts()
}
// 重置搜索
const resetSearch = () => {
searchQuery.value = ''
sortBy.value = 'name'
currentPage.value = 1
fetchContacts()
}
// 切换页码
const changePage = (page: number) => {
if (page < 1 || page > totalPages.value) return
currentPage.value = page
fetchContacts()
}
// 打开添加联系人模态框
const openAddContactModal = () => {
form.value = {
name: '',
title: '',
company: '',
toEmail: '',
ccEmails: [],
otherInfo: '',
}
newCcEmail.value = ''
errors.value = { name: '', toEmail: '' }
isEditing.value = false
showContactModal.value = true
}
// 关闭联系人模态框
const closeContactModal = () => {
showContactModal.value = false
}
// 编辑联系人
const editContact = (contact: Contact) => {
form.value = { ...contact }
newCcEmail.value = ''
errors.value = { name: '', toEmail: '' }
isEditing.value = true
showContactModal.value = true
}
// 验证表单
const validateForm = (): boolean => {
let isValid = true
errors.value = { name: '', toEmail: '' }
if (!form.value.name?.trim()) {
errors.value.name = '请输入联系人姓名'
isValid = false
}
if (!form.value.toEmail?.trim()) {
errors.value.toEmail = '请输入收件人邮箱'
isValid = false
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.value.toEmail)) {
errors.value.toEmail = '请输入有效的邮箱地址'
isValid = false
}
return isValid
}
// 添加抄送人邮箱
const addCcEmail = () => {
const email = newCcEmail.value.trim()
if (!email) return
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
showError('请输入有效的邮箱地址')
return
}
// 检查是否已存在
if (form.value.ccEmails?.includes(email)) {
showError('该邮箱已在抄送人列表中')
return
}
form.value.ccEmails = [...(form.value.ccEmails || []), email]
newCcEmail.value = ''
}
// 移除抄送人邮箱
const removeCcEmail = (index: number) => {
if (form.value.ccEmails) {
form.value.ccEmails.splice(index, 1)
}
}
// 保存联系人 - 使用api拦截器
const saveContact = async () => {
if (!validateForm()) return
try {
isSubmitting.value = true
const contactData = {
name: form.value.name,
title: form.value.title,
company: form.value.company,
toEmail: form.value.toEmail,
ccEmails: form.value.ccEmails,
otherInfo: form.value.otherInfo,
}
if (isEditing.value && form.value.id) {
// 更新现有联系人
await api.put(`/contacts/${form.value.id}`, contactData)
} else {
// 添加新联系人
await api.post('/contacts', contactData)
}
// 保存成功,刷新列表并关闭模态框
showSuccess(isEditing.value ? '联系人已更新' : '联系人已添加')
closeContactModal()
fetchContacts()
} catch (error) {
console.error('保存联系人失败:', error)
showError(error instanceof Error ? error.message : '保存联系人失败,请稍后重试')
} finally {
isSubmitting.value = false
}
}
// 删除联系人 - 使用api拦截器
const deleteContact = async (id: number) => {
if (!confirm('确定要删除这个联系人吗?此操作不可撤销。')) {
return
}
try {
await api.delete(`/contacts/${id}`)
showSuccess('联系人已删除')
fetchContacts()
} catch (error) {
console.error('删除联系人失败:', error)
showError('删除联系人失败,请稍后重试')
}
}
// 处理导入文件选择
const handleFileSelected = (e: Event) => {
const input = e.target as HTMLInputElement
if (!input.files || input.files.length === 0) return
importFile.value = input.files[0]
}
// 移除导入文件
const removeImportFile = () => {
importFile.value = null
// 重置文件输入
const input = document.getElementById('contact-import-file') as HTMLInputElement
if (input) input.value = ''
}
// 格式化文件大小
const formatFileSize = (bytes: number) => {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
// 提交导入文件 - 使用api拦截器
const submitImportFile = async () => {
if (!importFile.value) return
try {
isImporting.value = true
const formData = new FormData()
formData.append('file', importFile.value as File)
// 调用后端接口处理文件导入,会自动添加Authorization头
const result = await api.post('/contacts/import', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
// 导入成功,显示结果
importResult.value = {
success: true,
message: `导入完成,共处理 ${result.stats.success + result.stats.skipped + result.stats.failed} 条记录`,
stats: result.stats,
errors: result.errors,
}
showImportModal.value = false
showImportResultModal.value = true
// 刷新联系人列表
fetchContacts()
} catch (error) {
console.error('导入失败:', error)
importResult.value = {
success: false,
message: error instanceof Error ? error.message : '导入过程中发生错误,请稍后重试',
}
showImportModal.value = false
showImportResultModal.value = true
} finally {
isImporting.value = false
}
}
// 下载导入模板 - 使用api拦截器
const downloadImportTemplate = async () => {
try {
// 调用后端接口下载模板文件
const response = await api.get('/contacts/import/template', {
responseType: 'blob',
})
const blob = new Blob([response], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
})
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = '联系人导入模板.xlsx'
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
} catch (error) {
console.error('下载模板失败:', error)
showError('下载模板失败,请稍后重试')
}
}
// 关闭导入模态框
const closeImportModal = () => {
showImportModal.value = false
importFile.value = null
// 重置文件输入
const input = document.getElementById('contact-import-file') as HTMLInputElement
if (input) input.value = ''
}
// 关闭导入结果模态框
const closeImportResultModal = () => {
showImportResultModal.value = false
}
// 显示成功提示
const showSuccess = (message: string) => {
successMessage.value = message
showSuccessToast.value = true
setTimeout(() => {
showSuccessToast.value = false
}, 3000)
}
// 显示错误提示
const showError = (message: string) => {
errorMessage.value = message
showErrorToast.value = true
setTimeout(() => {
showErrorToast.value = false
}, 3000)
}
</script>
<style scoped>
/* 样式与之前保持一致 */
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.animate-slide-up {
animation: slideUp 0.3s ease-out forwards;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
.btn-primary {
@apply bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors font-medium;
}
.btn-outline {
@apply border border-gray-300 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-50 transition-colors font-medium;
}
tbody tr:hover {
@apply bg-gray-50;
}
.inline-flex.items-center.px-2.5.py-0.5.rounded-full.text-xs.font-medium.bg-blue-100.text-blue-800 {
@apply transition-all;
}
.inline-flex.items-center.px-2.5.py-0.5.rounded-full.text-xs.font-medium.bg-blue-100.text-blue-800:hover {
@apply bg-blue-200;
}
@media (min-width: 768px) {
.max-w-7xl {
max-width: 1140px;
}
}
@media (min-width: 1200px) {
.max-w-7xl {
max-width: 1320px;
}
}
</style>
<template>
<div
v-if="visible"
class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4"
>
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md">
<h3 class="text-lg font-semibold mb-4">{{ title }}</h3>
<input
type="file"
:accept="accept"
@change="handleFileSelect"
class="w-full px-3 py-2 border border-gray-300 rounded-md mb-4"
/>
<div class="flex justify-end gap-3">
<button
@click="handleCancel"
class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50"
>
取消
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
title: {
type: String,
default: '导入文件',
},
accept: {
type: String,
default: '.csv,.txt',
},
})
const emit = defineEmits(['update:visible', 'file-selected'])
const handleFileSelect = (e: Event) => {
const input = e.target as HTMLInputElement
if (input.files && input.files[0]) {
const file = input.files[0]
const reader = new FileReader()
reader.onload = (e) => {
const content = e.target?.result as string
emit('file-selected', { file, content })
emit('update:visible', false)
}
reader.readAsText(file)
}
}
const handleCancel = () => {
emit('update:visible', false)
}
// 监听visible变化,重置文件输入
watch(
() => props.visible,
(newVisible) => {
if (newVisible) {
// 下次DOM更新后重置文件输入
setTimeout(() => {
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement
if (fileInput) {
fileInput.value = ''
}
}, 100)
}
},
)
</script>
<template>
<div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-lg shadow-xl w-full max-w-4xl max-h-[80vh] flex flex-col">
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-lg font-semibold">导入记录管理</h3>
<button @click="$emit('close')">
<i class="fas fa-times text-gray-500"></i>
</button>
</div>
<div class="p-4 border-b border-gray-200">
<div class="flex gap-2">
<input
v-model="searchTerm"
type="text"
class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="搜索收件人邮箱..."
/>
<button
@click="clearSearch"
class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
>
清除
</button>
</div>
</div>
<div class="p-4 flex-1 overflow-y-auto">
<div v-if="filteredRecords.length === 0" class="text-center text-gray-500 py-8">
<p>未找到匹配的导入记录</p>
</div>
<div v-else class="space-y-3">
<div
v-for="record in filteredRecords"
:key="record.id"
class="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow"
>
<div class="flex justify-between items-start mb-3">
<div class="flex-1">
<div class="font-medium text-gray-900">收件人: {{ record.to }}</div>
<div class="text-sm text-gray-600 mt-1">抄送人: {{ record.cc || '无' }}</div>
<div class="text-xs text-gray-400 mt-2">
创建时间: {{ formatDate(record.createdAt) }}
</div>
</div>
<div class="flex gap-2">
<button
@click="editRecord(record)"
class="px-3 py-1 bg-blue-100 text-blue-700 rounded-md hover:bg-blue-200 transition-colors text-sm"
>
编辑
</button>
<button
@click="deleteRecord(record.id)"
class="px-3 py-1 bg-red-100 text-red-700 rounded-md hover:bg-red-200 transition-colors text-sm"
>
删除
</button>
</div>
</div>
<div v-if="editingRecordId === record.id" class="mt-3 p-3 bg-gray-50 rounded-md">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">收件人</label>
<input
v-model="editingRecord.to"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="收件人邮箱"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">抄送人</label>
<input
v-model="editingRecord.cc"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="抄送人邮箱,多个用逗号分隔"
/>
</div>
</div>
<div class="flex justify-end gap-2 mt-3">
<button
@click="cancelEdit"
class="px-3 py-1 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors text-sm"
>
取消
</button>
<button
@click="saveEdit"
class="px-3 py-1 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors text-sm"
>
保存
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 border-t border-gray-200">
<button
@click="$emit('close')"
class="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"
>
关闭
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, defineProps, defineEmits } from 'vue'
import { ImportRecord } from '../types'
const props = defineProps({
records: {
type: Array as () => ImportRecord[],
required: true,
},
})
const emits = defineEmits(['update-record', 'delete-record', 'close'])
const searchTerm = ref('')
const editingRecordId = ref<string | null>(null)
const editingRecord = ref<Partial<ImportRecord>>({})
const filteredRecords = computed(() => {
if (!searchTerm.value) return props.records
return props.records.filter(
(record) =>
record.to.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
record.cc.toLowerCase().includes(searchTerm.value.toLowerCase()),
)
})
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString('zh-CN')
}
const editRecord = (record: ImportRecord) => {
editingRecordId.value = record.id
editingRecord.value = { ...record }
}
const cancelEdit = () => {
editingRecordId.value = null
editingRecord.value = {}
}
const saveEdit = () => {
if (editingRecordId.value && editingRecord.value.to) {
emits('update-record', {
id: editingRecordId.value,
to: editingRecord.value.to,
cc: editingRecord.value.cc || '',
updatedAt: new Date().toISOString(),
})
editingRecordId.value = null
editingRecord.value = {}
}
}
const deleteRecord = (id: string) => {
if (confirm('确定要删除这条导入记录吗?')) {
emits('delete-record', id)
}
}
const clearSearch = () => {
searchTerm.value = ''
}
</script>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<div> <div>
<div class="bg-white rounded-lg shadow-md p-6 mb-6"> <div class="bg-white rounded-lg shadow-md p-6 mb-6">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">添加变量</h3> <h3 class="text-lg font-semibold">添加/编辑变量</h3>
<button <button
v-if="editingVariableId" v-if="editingVariableId"
@click="resetVariableForm" @click="resetVariableForm"
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
<div> <div>
<label class="block text-gray-700 mb-1 text-sm">变量名称 *</label> <label class="block text-gray-700 mb-1 text-sm">变量名称 *</label>
<input <input
v-model="variableForm.name" v-model="variableForm.variableNameCn"
type="text" type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="例如:用户名" placeholder="例如:用户名"
...@@ -30,14 +30,20 @@ ...@@ -30,14 +30,20 @@
{{ variablePrefix }} {{ variablePrefix }}
</span> </span>
<input <input
v-model="variableForm.key" v-model="variableForm.variableNameEn"
type="text" type="text"
class="flex-1 px-3 py-2 border border-gray-300 rounded-r-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" class="flex-1 px-3 py-2 border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="例如:username" placeholder="例如:username"
/> />
<span
class="inline-flex items-center px-3 py-2 rounded-r-md border border-l-0 border-gray-300 bg-gray-50 text-gray-500"
>
{{ variableNextfix }}
</span>
</div> </div>
<p class="text-xs text-gray-500 mt-1"> <p class="text-xs text-gray-500 mt-1">
变量将以 {{ variablePrefix }}key 形式插入邮件内容 变量将以 {{ variablePrefix }}{{ variableForm.variableNameEn
}}{{ variableNextfix }} 形式插入邮件内容
</p> </p>
</div> </div>
<div class="md:col-span-2"> <div class="md:col-span-2">
...@@ -54,7 +60,7 @@ ...@@ -54,7 +60,7 @@
<button <button
@click="saveVariable" @click="saveVariable"
class="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-700 transition-colors" class="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-700 transition-colors"
:disabled="!variableForm.name || !variableForm.key" :disabled="!variableForm.variableNameCn || !variableForm.variableNameEn"
> >
{{ editingVariableId ? '更新变量' : '添加变量' }} {{ editingVariableId ? '更新变量' : '添加变量' }}
</button> </button>
...@@ -99,11 +105,11 @@ ...@@ -99,11 +105,11 @@
<p class="text-sm text-gray-600 mb-3">{{ template.description || '无描述' }}</p> <p class="text-sm text-gray-600 mb-3">{{ template.description || '无描述' }}</p>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<span <span
v-for="variableId in template.variableIds" v-for="variableId in template.variableBizIdList"
:key="variableId" :key="variableId"
class="px-2 py-1 bg-blue-50 text-blue-700 rounded text-xs" class="px-2 py-1 bg-blue-50 text-blue-700 rounded text-xs"
> >
{{ variablePrefix }}{{ getVariableKeyById(variableId) }} {{ variablePrefix }}{{ getVariableKeyById(variableId) }}{{ variableNextfix }}
</span> </span>
</div> </div>
<div class="mt-3 flex justify-end"> <div class="mt-3 flex justify-end">
...@@ -155,9 +161,9 @@ ...@@ -155,9 +161,9 @@
</thead> </thead>
<tbody class="bg-white divide-y divide-gray-200"> <tbody class="bg-white divide-y divide-gray-200">
<tr v-for="variable in variables" :key="variable.id"> <tr v-for="variable in variables" :key="variable.id">
<td class="px-6 py-4 whitespace-nowrap">{{ variable.name }}</td> <td class="px-6 py-4 whitespace-nowrap">{{ variable.variableNameCn }}</td>
<td class="px-6 py-4 whitespace-nowrap font-mono text-sm"> <td class="px-6 py-4 whitespace-nowrap font-mono text-sm">
{{ variablePrefix }}{{ variable.key }} {{ variablePrefix }}{{ variable.variableNameEn }}{{ variableNextfix }}
</td> </td>
<td class="px-6 py-4">{{ variable.description || '-' }}</td> <td class="px-6 py-4">{{ variable.description || '-' }}</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
...@@ -168,7 +174,7 @@ ...@@ -168,7 +174,7 @@
编辑 编辑
</button> </button>
<button <button
@click="deleteVariable(variable.id)" @click="deleteVariable(variable.variableBizId || '')"
class="text-red-600 hover:text-red-900" class="text-red-600 hover:text-red-900"
> >
删除 删除
...@@ -227,16 +233,16 @@ ...@@ -227,16 +233,16 @@
> >
<input <input
type="checkbox" type="checkbox"
:id="'template-var-' + variable.id" :id="'template-var-' + variable.variableBizId"
:checked="templateForm.variableIds?.includes(variable.id)" :checked="templateForm.variableBizIdList?.includes(variable.variableBizId || '')"
class="mr-3" class="mr-3"
@change="toggleTemplateVariable(variable.id)" @change="toggleTemplateVariable(variable.variableBizId || '')"
/> />
<label for="'template-var-' + variable.id"> <label for="'template-var-' + variable.variableBizId">
<div class="font-medium font-mono text-sm"> <div class="font-medium font-mono text-sm">
{{ variablePrefix }}{{ variable.key }} {{ variablePrefix }}{{ variable.variableNameEn }}{{ variableNextfix }}
</div> </div>
<div class="text-xs text-gray-500">{{ variable.name }}</div> <div class="text-xs text-gray-500">{{ variable.variableNameCn }}</div>
</label> </label>
</div> </div>
</div> </div>
...@@ -256,9 +262,9 @@ ...@@ -256,9 +262,9 @@
@click="saveVariableTemplate" @click="saveVariableTemplate"
class="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-700 transition-colors" class="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-700 transition-colors"
:disabled=" :disabled="
!templateForm.name || !templateForm.groupName ||
!templateForm.variableIds || !templateForm.variableBizIdList ||
templateForm.variableIds.length === 0 templateForm.variableBizIdList.length === 0
" "
> >
{{ editingTemplateId ? '更新模板' : '创建模板' }} {{ editingTemplateId ? '更新模板' : '创建模板' }}
...@@ -267,11 +273,60 @@ ...@@ -267,11 +273,60 @@
</div> </div>
</div> </div>
</div> </div>
<CommonModal
v-model:visible="modalVisible"
:title="modalConfig.title"
type="confirm"
:message="modalConfig.message"
:show-cancel-button="modalConfig.showCancel"
@confirm="handleConfirm"
@cancel="handleCancel"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, defineProps, defineEmits } from 'vue' import { ref, defineProps, defineEmits, onMounted } from 'vue'
import { Variable, VariableTemplate } from '../types' import type { Variable, VariableTemplate } from '../types'
import {
getEmailVariableList,
addEmailVariable,
editEmailVariable,
deleteEmailVariable,
addEmailVariableGroup,
editEmailVariableGroup,
deleteEmailVariableGroup,
getEmailVariableGroupList,
} from '@/api/api'
// 引入弹窗组件
import CommonModal from '@/components/CommonModal.vue'
// 弹窗提示信息对象
const modalVisible = ref(false)
const modalConfig = ref({
showCancel: false,
title: '操作确认',
message: '确定要执行此操作吗?',
})
const openModal = (config: { showCancel?: boolean; title?: string; message?: string } = {}) => {
modalConfig.value = {
showCancel: config.showCancel ?? true,
title: config.title ?? '操作确认',
message: config.message ?? '确定要执行此操作吗?',
}
modalVisible.value = true
}
const handleConfirm = () => {
modalVisible.value = false
console.log('用户确认操作')
}
const handleCancel = () => {
modalVisible.value = false
console.log('用户取消操作')
}
const props = defineProps({ const props = defineProps({
variables: { variables: {
...@@ -290,12 +345,14 @@ const emits = defineEmits(['update-variables', 'update-variable-templates']) ...@@ -290,12 +345,14 @@ const emits = defineEmits(['update-variables', 'update-variable-templates'])
const variables = ref<Variable[]>([...props.variables]) const variables = ref<Variable[]>([...props.variables])
const variableTemplates = ref<VariableTemplate[]>([...props.variableTemplates]) const variableTemplates = ref<VariableTemplate[]>([...props.variableTemplates])
const variablePrefix = '{{' const variablePrefix = '{{'
const variableNextfix = '}}'
// 变量表单 // 变量表单
const editingVariableId = ref('') const editingVariableId = ref('')
const variableForm = ref<Partial<Variable>>({ const variableForm = ref<Partial<Variable>>({
name: '', variableBizId: '',
key: '', variableNameCn: '',
variableNameEn: '',
description: '', description: '',
}) })
...@@ -303,80 +360,169 @@ const variableForm = ref<Partial<Variable>>({ ...@@ -303,80 +360,169 @@ const variableForm = ref<Partial<Variable>>({
const showTemplateModal = ref(false) const showTemplateModal = ref(false)
const editingTemplateId = ref('') const editingTemplateId = ref('')
const templateForm = ref<Partial<VariableTemplate>>({ const templateForm = ref<Partial<VariableTemplate>>({
name: '', groupName: '',
description: '', description: '',
variableIds: [], variableBizIdList: [],
})
// 在组件挂载时自动获取变量列表和模版列表
onMounted(() => {
fetchVariables()
fetchVariableTemplates()
}) })
// 方法 - 变量管理 // 方法 - 变量管理
const resetVariableForm = () => { const resetVariableForm = () => {
editingVariableId.value = '' editingVariableId.value = ''
variableForm.value = { variableForm.value = {
name: '', variableBizId: '',
key: '', variableNameCn: '',
variableNameEn: '',
description: '', description: '',
} }
} }
const saveVariable = () => { const saveVariable = () => {
if (!variableForm.value.name || !variableForm.value.key) return if (!variableForm.value.variableNameCn || !variableForm.value.variableNameEn) return
// 检查变量标识是否已存在
const exists = variables.value.some(
(v) => v.key === variableForm.value.key && v.id !== editingVariableId.value,
)
if (exists) {
alert('变量标识已存在,请使用其他标识')
return
}
if (editingVariableId.value) { if (editingVariableId.value) {
// 更新现有变量 // 更新接口
const index = variables.value.findIndex((v) => v.id === editingVariableId.value) editEmailVariable({
if (index > -1) { variableBizId: variableForm.value.variableBizId || '',
variables.value[index] = { variableNameCn: variableForm.value.variableNameCn,
...variables.value[index], variableNameEn: variableForm.value.variableNameEn,
...variableForm.value, description: variableForm.value.description,
} as Variable }).then(() => {
emits('update-variables', [...variables.value]) // 更新本地变量列表
alert('变量更新成功') const index = variables.value.findIndex((v) => v.id === editingVariableId.value)
} if (index > -1) {
variables.value[index] = {
...variables.value[index],
...variableForm.value,
}
}
fetchVariables()
openModal({
title: '成功',
message: '变量更新成功',
})
})
} else { } else {
// 添加新变量 // 创建接口
const newVariable: Variable = { addEmailVariable({
id: Date.now().toString(), variableNameCn: variableForm.value.variableNameCn,
name: variableForm.value.name || '', variableNameEn: variableForm.value.variableNameEn,
key: variableForm.value.key || '', description: variableForm.value.description,
description: variableForm.value.description || '', })
} .then((res) => {
// 刷新变量列表
variables.value.push(newVariable) fetchVariables()
emits('update-variables', [...variables.value]) openModal({
alert('变量添加成功') title: '成功',
message: '变量创建成功',
})
})
.catch((error) => {
console.error('创建变量失败:', error)
if (error.response?.data?.message) {
openModal({
title: '错误',
message: error.response.data.message,
})
} else {
openModal({
title: '错误',
message: '创建变量失败',
})
}
})
} }
resetVariableForm() resetVariableForm()
} }
const editVariable = (variable: Variable) => { const editVariable = (variable: Variable) => {
editingVariableId.value = variable.id editingVariableId.value = variable.variableBizId || ''
variableForm.value = { ...variable } variableForm.value = { ...variable }
} }
const deleteVariable = (id: string) => { const deleteVariable = (id: string) => {
if (confirm('确定要删除这个变量吗?这可能会影响使用该变量的模板。')) { if (confirm('确定要删除这个变量吗?这可能会影响使用该变量的模板。')) {
// 从变量列表中删除 // 删除接口
variables.value = variables.value.filter((variable) => variable.id !== id) deleteEmailVariable(id)
emits('update-variables', [...variables.value]) .then(() => {
// 刷新变量列表
// 从所有模板中移除该变量 fetchVariables()
variableTemplates.value = variableTemplates.value.map((template) => ({ openModal({
...template, title: '成功',
variableIds: template.variableIds.filter((vid) => vid !== id), message: '变量删除成功',
})) })
emits('update-variable-templates', [...variableTemplates.value]) })
.catch((error) => {
console.error('删除变量失败:', error)
if (error.response?.data?.message) {
openModal({
title: '错误',
message: error.response.data.message,
})
} else {
openModal({
title: '错误',
message: '删除变量失败',
})
}
})
}
}
// 查询变量列表
const fetchVariables = () => {
const params = {
pageNo: 1,
pageSize: 10,
}
getEmailVariableList(params)
.then((res) => {
variables.value = res.data.records || []
})
.catch((error) => {
console.error('查询变量列表失败:', error)
if (error.response?.data?.message) {
openModal({
title: '错误',
message: error.response.data.message,
})
} else {
openModal({
title: '错误',
message: '查询变量列表失败',
})
}
})
}
// 查询变量模版列表
const fetchVariableTemplates = () => {
const params = {
pageNo: 1,
pageSize: 10,
} }
getEmailVariableGroupList(params)
.then((res) => {
variableTemplates.value = res.data.records || []
})
.catch((error) => {
console.error('查询变量模版列表失败:', error)
if (error.response?.data?.message) {
openModal({
title: '错误',
message: error.response.data.message,
})
} else {
openModal({
title: '错误',
message: '查询变量模版列表失败',
})
}
})
} }
// 方法 - 模板管理 // 方法 - 模板管理
...@@ -386,9 +532,9 @@ const showCreateTemplateModal = (isNew: boolean) => { ...@@ -386,9 +532,9 @@ const showCreateTemplateModal = (isNew: boolean) => {
if (isNew) { if (isNew) {
editingTemplateId.value = '' editingTemplateId.value = ''
templateForm.value = { templateForm.value = {
name: '', groupName: '',
description: '', description: '',
variableIds: [], variableBizIdList: [],
} }
} }
} }
...@@ -403,30 +549,30 @@ const closeTemplateModal = () => { ...@@ -403,30 +549,30 @@ const closeTemplateModal = () => {
showTemplateModal.value = false showTemplateModal.value = false
editingTemplateId.value = '' editingTemplateId.value = ''
templateForm.value = { templateForm.value = {
name: '', groupName: '',
description: '', description: '',
variableIds: [], variableBizIdList: [],
} }
} }
const toggleTemplateVariable = (variableId: string) => { const toggleTemplateVariable = (variableId: string) => {
if (!templateForm.value.variableIds) { if (!templateForm.value.variableBizIdList) {
templateForm.value.variableIds = [] templateForm.value.variableBizIdList = []
} }
const index = templateForm.value.variableIds.indexOf(variableId) const index = templateForm.value.variableBizIdList.indexOf(variableId)
if (index > -1) { if (index > -1) {
templateForm.value.variableIds.splice(index, 1) templateForm.value.variableBizIdList.splice(index, 1)
} else { } else {
templateForm.value.variableIds.push(variableId) templateForm.value.variableBizIdList.push(variableId)
} }
} }
const saveVariableTemplate = () => { const saveVariableTemplate = () => {
if ( if (
!templateForm.value.name || !templateForm.value.groupName ||
!templateForm.value.variableIds || !templateForm.value.variableBizIdList ||
templateForm.value.variableIds.length === 0 templateForm.value.variableBizIdList.length === 0
) )
return return
...@@ -436,9 +582,9 @@ const saveVariableTemplate = () => { ...@@ -436,9 +582,9 @@ const saveVariableTemplate = () => {
if (index > -1) { if (index > -1) {
variableTemplates.value[index] = { variableTemplates.value[index] = {
id: editingTemplateId.value, id: editingTemplateId.value,
name: templateForm.value.name || '', groupName: templateForm.value.groupName || '',
description: templateForm.value.description || '', description: templateForm.value.description || '',
variableIds: templateForm.value.variableIds || [], variableBizIdList: templateForm.value.variableBizIdList || [],
} }
emits('update-variable-templates', [...variableTemplates.value]) emits('update-variable-templates', [...variableTemplates.value])
} }
...@@ -446,9 +592,9 @@ const saveVariableTemplate = () => { ...@@ -446,9 +592,9 @@ const saveVariableTemplate = () => {
// 创建新模板 // 创建新模板
const newTemplate: VariableTemplate = { const newTemplate: VariableTemplate = {
id: Date.now().toString(), id: Date.now().toString(),
name: templateForm.value.name || '', groupName: templateForm.value.groupName || '',
description: templateForm.value.description || '', description: templateForm.value.description || '',
variableIds: templateForm.value.variableIds || [], variableBizIdList: templateForm.value.variableBizIdList || [],
} }
variableTemplates.value.push(newTemplate) variableTemplates.value.push(newTemplate)
...@@ -456,7 +602,10 @@ const saveVariableTemplate = () => { ...@@ -456,7 +602,10 @@ const saveVariableTemplate = () => {
} }
closeTemplateModal() closeTemplateModal()
alert(editingTemplateId.value ? '模板更新成功' : '模板创建成功') openModal({
title: '成功',
message: editingTemplateId.value ? '模板更新成功' : '模板创建成功',
})
} }
const deleteVariableTemplate = (id: string) => { const deleteVariableTemplate = (id: string) => {
...@@ -468,17 +617,20 @@ const deleteVariableTemplate = (id: string) => { ...@@ -468,17 +617,20 @@ const deleteVariableTemplate = (id: string) => {
const getVariableKeyById = (id: string) => { const getVariableKeyById = (id: string) => {
const variable = variables.value.find((v) => v.id === id) const variable = variables.value.find((v) => v.id === id)
return variable?.key || '' return variable?.variableNameEn || ''
} }
const generateExcelTemplate = (template: VariableTemplate) => { const generateExcelTemplate = (template: VariableTemplate) => {
// 模拟生成Excel模板 // 模拟生成Excel模板
const variableNames = template.variableIds.map((id) => { const variableNames = (template.variableBizIdList || []).map((id) => {
const variable = variables.value.find((v) => v.id === id) const variable = variables.value.find((v) => v.id === id)
return variable ? variable.name : '' return variable ? variable.variableNameEn || '' : ''
}) })
alert(`已生成包含以下变量的Excel模板:\n${variableNames.join(', ')}`) openModal({
title: '成功',
message: `已生成包含以下变量的Excel模板:\n${variableNames.join(', ')}`,
})
// 实际项目中这里应该生成并下载Excel文件 // 实际项目中这里应该生成并下载Excel文件
} }
</script> </script>
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
"exclude": ["src/**/__tests__/*"], "exclude": ["src/**/__tests__/*"],
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
} }
......
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx' import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools' import vueDevTools from 'vite-plugin-vue-devtools'
/**设置server转发 */
// https://vite.dev/config/
export default defineConfig({ export default defineConfig({
// 关键配置:设置基础路径为子目录 yd-email // 关键配置:设置基础路径为子目录 yd-email
// 生产环境(部署到服务器)用 '/yd-email/',本地开发用 '/'(避免开发时路径错误)
base: process.env.NODE_ENV === 'production' ? '/yd-email/' : '/', base: process.env.NODE_ENV === 'production' ? '/yd-email/' : '/',
plugins: [vue(), vueJsx(), vueDevTools()], plugins: [vue(), vueJsx(), vueDevTools()],
resolve: { resolve: {
...@@ -16,4 +14,20 @@ export default defineConfig({ ...@@ -16,4 +14,20 @@ export default defineConfig({
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),
}, },
}, },
// 添加CSS配置
css: {
postcss: './postcss.config.js',
},
server: {
port: 5173,
host: 'localhost',
open: true,
proxy: {
'email/api': {
target: 'http://139.224.145.34:9002',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/email\/api/, ''),
},
},
},
}) })
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment