create-swdg-frontend 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-swdg-frontend",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Scaffold a new SWDG frontend project",
5
5
  "type": "module",
6
6
  "bin": {
package/template/.env ADDED
@@ -0,0 +1 @@
1
+ VITE_DEMO_PROXY_TARGET=http://127.0.0.1:8888
@@ -29,6 +29,7 @@ export default [
29
29
  '.env.*.local',
30
30
  'src/components.d.ts',
31
31
  'public/**',
32
+ 'src/api/**',
32
33
  'dev-dist',
33
34
  ],
34
35
  },
@@ -33,10 +33,7 @@
33
33
  ]
34
34
  },
35
35
  "dependencies": {
36
- "amfe-flexible": "^2.2.1",
37
36
  "axios": "^1.13.2",
38
- "echarts": "^6.0.0",
39
- "element-plus": "^2.13.1",
40
37
  "lint-staged": "^16.2.7",
41
38
  "pinia": "^3.0.4",
42
39
  "pinia-plugin-persistedstate": "^4.7.1",
@@ -64,8 +61,6 @@
64
61
  "husky": "^9.1.7",
65
62
  "jsdom": "^27.4.0",
66
63
  "less": "^4.5.1",
67
- "less-loader": "^12.3.0",
68
- "postcss-pxtorem": "^6.1.0",
69
64
  "prettier": "^3.8.0",
70
65
  "rollup-plugin-copy": "^3.5.0",
71
66
  "sass-embedded": "^1.97.2",
@@ -0,0 +1,5 @@
1
+ export default {
2
+ plugins: {
3
+ autoprefixer: {},
4
+ },
5
+ };
@@ -9,6 +9,9 @@ function fixTypeName(name) {
9
9
  }
10
10
 
11
11
  function fixSwagger(doc) {
12
+ if (Array.isArray(doc.servers)) {
13
+ doc.servers = [{ url: '' }];
14
+ }
12
15
  if (doc.components && doc.components.schemas) {
13
16
  for (const [key, schema] of Object.entries(doc.components.schemas)) {
14
17
  if (key.includes(' ')) {
@@ -200,6 +203,36 @@ function moveToSrcApi(serviceName, tempDir, targetBase) {
200
203
  console.log(`✅ ${serviceName} 接口生成完毕,已移动到 src/api/${serviceName}`);
201
204
  }
202
205
 
206
+ function toCamelExportName(serviceName) {
207
+ const parts = serviceName.split(/[^a-zA-Z0-9]+/).filter(Boolean);
208
+ if (parts.length === 0) return serviceName;
209
+ const [first, ...rest] = parts;
210
+ return (
211
+ first.toLowerCase() +
212
+ rest.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('')
213
+ );
214
+ }
215
+
216
+ function toEnvKey(serviceName) {
217
+ return serviceName.replace(/[^a-zA-Z0-9]+/g, '_').toUpperCase();
218
+ }
219
+
220
+ function writeServiceClient(serviceName, targetBase) {
221
+ const destDir = path.join(targetBase, serviceName);
222
+ const envKey = `VITE_${toEnvKey(serviceName)}_API_BASE`;
223
+ const content = `import { Configuration } from "@/api/${serviceName}/configuration";\nimport http from "@/utils/http";\n\nconst env = import.meta.env as Record<string, string | undefined>;\nconst apiBase = (env[\"${envKey}\"] ?? import.meta.env.VITE_API_BASE ?? \"\").replace(/\\/+$/, \"\");\nconst configuration = new Configuration({\n basePath: apiBase,\n});\n\ntype ApiConstructor<T> = new (\n config?: Configuration,\n basePath?: string,\n axios?: typeof http,\n) => T;\n\nexport const createApi = <T>(ApiClass: ApiConstructor<T>): T =>\n new ApiClass(configuration, undefined, http);\n\nexport const apiConfig = configuration;\nexport { http };\n`;
224
+ fs.writeFileSync(path.join(destDir, 'client.ts'), content, 'utf-8');
225
+ }
226
+
227
+ function updateApiIndex(services, targetBase) {
228
+ const lines = services.map(
229
+ (svc) =>
230
+ `export * as ${toCamelExportName(svc.name)} from "@/api/${svc.name}/client";`
231
+ );
232
+ const content = `${lines.join('\n')}\n`;
233
+ fs.writeFileSync(path.join(targetBase, 'index.ts'), content, 'utf-8');
234
+ }
235
+
203
236
  async function generateService(serviceName, apiUrl) {
204
237
  const tempPath = path.join(__dirname, '.tmp', serviceName);
205
238
  const fixedJsonPath = path.join(tempPath, 'swagger-fixed.json');
@@ -207,7 +240,9 @@ async function generateService(serviceName, apiUrl) {
207
240
  fs.mkdirSync(tempPath, { recursive: true });
208
241
  await fetchAndFixSwagger(apiUrl, fixedJsonPath);
209
242
  generateClient(fixedJsonPath, tempPath);
210
- moveToSrcApi(serviceName, tempPath, path.resolve(__dirname, '../src/api'));
243
+ const apiBaseDir = path.resolve(__dirname, '../src/api');
244
+ moveToSrcApi(serviceName, tempPath, apiBaseDir);
245
+ writeServiceClient(serviceName, apiBaseDir);
211
246
  }
212
247
 
213
248
  async function main() {
@@ -225,6 +260,8 @@ async function main() {
225
260
  console.error(`❌ 生成失败: ${svc.name}`, err && err.message ? err.message : err);
226
261
  }
227
262
  }
263
+
264
+ updateApiIndex(services, path.resolve(__dirname, '../src/api'));
228
265
  }
229
266
 
230
267
  main();
@@ -1,16 +1,50 @@
1
1
  <template>
2
2
  <div class="body-wrapper">I am body</div>
3
- <el-button type="primary" @click="handleClick"> go to test page </el-button>
4
- <el-button type="primary" @click="handleClick"> go to test page </el-button>
3
+ <button id="body-button" @click="handleClick">go to test page</button>
4
+ <button id="body-button-inc" @click="handleClickInc">+1</button>
5
5
  </template>
6
6
  <script setup lang="ts">
7
7
  import { useRouter } from 'vue-router';
8
+ import { useCounterStore } from '@stores/useCounter';
8
9
 
10
+ const counter = useCounterStore();
9
11
  const router = useRouter();
10
12
 
11
13
  const handleClick = () => {
12
14
  router.push('/test');
13
15
  };
16
+
17
+ const handleClickInc = () => {
18
+ counter.inc();
19
+ };
14
20
  </script>
15
21
 
16
- <style scoped></style>
22
+ <style scoped>
23
+ #body-button-test {
24
+ margin-top: 20px;
25
+ padding: 10px 20px;
26
+ border-radius: 5px;
27
+ border: 1px solid #ccc;
28
+ background-color: #f0f0f0;
29
+ color: #333;
30
+ cursor: pointer;
31
+ }
32
+
33
+ #body-button-inc {
34
+ margin-top: 20px;
35
+ padding: 10px 20px;
36
+ border-radius: 5px;
37
+ border: 1px solid #ccc;
38
+ background-color: #f0f0f0;
39
+ color: #333;
40
+ cursor: pointer;
41
+ }
42
+
43
+ #body-button-test:hover {
44
+ background-color: #e0e0e0;
45
+ }
46
+
47
+ #body-button-inc:hover {
48
+ background-color: #e0e0e0;
49
+ }
50
+ </style>
@@ -4,12 +4,7 @@ import { createPinia } from 'pinia';
4
4
  import './styles/global.css';
5
5
  import './styles/alerts.less';
6
6
 
7
- import ElementPlus from 'element-plus';
8
- import zhCn from 'element-plus/es/locale/lang/zh-cn';
9
- import 'element-plus/dist/index.css';
10
-
11
7
  import App from './App.vue';
12
- import 'amfe-flexible';
13
8
  import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
14
9
 
15
10
  import router from '@/router';
@@ -20,10 +15,6 @@ import focus from '@/directive/focus';
20
15
  const app = createApp(App);
21
16
  const pinia = createPinia();
22
17
  pinia.use(piniaPluginPersistedstate);
23
-
24
- app.use(ElementPlus, {
25
- locale: zhCn,
26
- });
27
18
  app.use(pinia);
28
19
  app.use(router);
29
20
  app.use(globalComponents);
@@ -0,0 +1,180 @@
1
+ /**
2
+ * @file axios-middleware.js
3
+ * @description 该模块为 Axios 提供设置全局请求和响应拦截器的工具。
4
+ * 主要功能包括:
5
+ * - 缓存机制:减少重复请求,提高性能;
6
+ * - 错误处理:包括业务逻辑错误和网络错误的统一处理;
7
+ * - 基于令牌的身份验证:在请求中自动附加身份验证信息。
8
+ * @author inupedia
9
+ */
10
+
11
+ import axios from 'axios';
12
+ import { useAuthStore } from '@/stores/authStore';
13
+
14
+ let vueRouter;
15
+
16
+ let globalReqInterceptor = null;
17
+ let globalRespInterceptor = null;
18
+
19
+ const TTL_CACHE_PERIOD = 500; // 默认0.5秒
20
+
21
+ const requestCache = new Map();
22
+
23
+ const generateCacheKey = (config) => {
24
+ const base = config.url;
25
+ if (config.method === 'post') {
26
+ return `${base}-${JSON.stringify(config.data)}`;
27
+ } else if (config.method === 'get' && config.params) {
28
+ return `${base}-${JSON.stringify(config.params)}`;
29
+ }
30
+ return base;
31
+ };
32
+
33
+ const setCache = (key, data, ttl = TTL_CACHE_PERIOD) => {
34
+ const existingCache = requestCache.get(key);
35
+
36
+ if (existingCache && existingCache.expiry > Date.now()) {
37
+ return;
38
+ }
39
+
40
+ const expiry = Date.now() + ttl;
41
+ requestCache.set(key, { data, expiry });
42
+ };
43
+
44
+ const getCache = (key) => {
45
+ const cached = requestCache.get(key);
46
+ if (cached && cached.expiry > Date.now()) {
47
+ return cached.data;
48
+ }
49
+ requestCache.delete(key);
50
+ return null;
51
+ };
52
+
53
+ const extractApiPath = (url) => {
54
+ const match = url.match(/\/v2\/(.*)/);
55
+ return match ? match[1] : 'Unknown API';
56
+ };
57
+
58
+ const handleBusinessError = (response) => {
59
+ if (response.data.code === 0) {
60
+ const apiPath = extractApiPath(response.config.url);
61
+ if (process.env.NODE_ENV === 'development') {
62
+ alert(`接口错误: ${apiPath}`);
63
+ }
64
+ }
65
+ };
66
+
67
+ const handleError = (error) => {
68
+ if (window == undefined) {
69
+ //console.error('代码在非浏览器环境中运行,无法访问 window 对象');
70
+ alert('浏览器版本过低,请升级浏览器后访问');
71
+ return Promise.reject(new Error('浏览器版本过低,请升级浏览器后访问'));
72
+ }
73
+ if (!error?.response || error.isOffline || error.code == 'ERR_NETWORK') {
74
+ //alert('网络错误');
75
+ return Promise.reject(new Error('网络错误'));
76
+ }
77
+ const authStore = useAuthStore();
78
+ if (error.response?.status == 401 && !window.hasShownUnauthorizedAlert) {
79
+ alert('您的登录信息已过期,请重新登录');
80
+ authStore.clearToken();
81
+ window.hasShownUnauthorizedAlert = true; // 设置标志为true,表示已经弹出过提示
82
+ vueRouter.push('/login');
83
+ } else if (error.response?.status == 403) {
84
+ alert('您没有权限访问该资源');
85
+ } else if (error.response?.status == 500) {
86
+ //alert('服务器内部错误');
87
+ } else if (error.response?.status == 400) {
88
+ const errorMessage = error.response.data?.message || '参数错误';
89
+ alert(errorMessage);
90
+ }
91
+ return Promise.reject(error);
92
+ };
93
+
94
+ const checkOnlineStatus = () => {
95
+ return navigator.onLine;
96
+ };
97
+
98
+ const setXTokenInterceptor = (token) => {
99
+ // 移除已存在的拦截器
100
+ if (globalReqInterceptor !== null) {
101
+ axios.interceptors.request.eject(globalReqInterceptor);
102
+ }
103
+ if (globalRespInterceptor !== null) {
104
+ axios.interceptors.response.eject(globalRespInterceptor);
105
+ }
106
+
107
+ // 设置新的请求拦截器
108
+ globalReqInterceptor = axios.interceptors.request.use(
109
+ (config) => {
110
+ if (!checkOnlineStatus()) {
111
+ return Promise.reject(new Error('网络连接不可用,请检查您的网络设置'));
112
+ }
113
+
114
+ if (!token) {
115
+ return config;
116
+ }
117
+
118
+ config.headers['X-APP-ID'] = 'selectsys';
119
+ config.headers['X-TOKEN'] = token;
120
+
121
+ const cacheKey = generateCacheKey(config);
122
+ const cachedData = getCache(cacheKey);
123
+
124
+ if (cachedData && config.method.toLowerCase() === 'get') {
125
+ config.adapter = () =>
126
+ Promise.resolve({
127
+ data: cachedData.data,
128
+ status: cachedData.status,
129
+ statusText: cachedData.statusText,
130
+ headers: cachedData.headers,
131
+ config,
132
+ request: cachedData.request,
133
+ });
134
+ }
135
+
136
+ return config;
137
+ },
138
+ (error) => handleError(error)
139
+ );
140
+
141
+ // 设置新的响应拦截器
142
+ globalRespInterceptor = axios.interceptors.response.use(
143
+ (response) => {
144
+ //handleBusinessError(response);
145
+
146
+ // 只缓存GET请求的响应数据
147
+ if (response.config.method.toLowerCase() === 'get') {
148
+ const cacheKey = generateCacheKey(response.config);
149
+ setCache(cacheKey, {
150
+ data: response.data,
151
+ status: response.status,
152
+ statusText: response.statusText,
153
+ headers: response.headers,
154
+ config: response.config,
155
+ request: response.request,
156
+ });
157
+ }
158
+
159
+ return response;
160
+ },
161
+ (error) => handleError(error)
162
+ );
163
+ };
164
+
165
+ export const setInterceptorVueRouter = (router) => {
166
+ if (router) {
167
+ console.log('拦截器router设置成功');
168
+ vueRouter = router;
169
+ } else {
170
+ console.warn('拦截器router设置失败,自动跳转功能不可用');
171
+ }
172
+ };
173
+
174
+ export const initAxiosInterceptors = (token) => {
175
+ if (token) {
176
+ console.log('拦截器token设置成功');
177
+ setXTokenInterceptor(token);
178
+ }
179
+ window.hasShownUnauthorizedAlert = false;
180
+ };
@@ -0,0 +1,5 @@
1
+ import axios from 'axios';
2
+
3
+ const http = axios.create();
4
+
5
+ export default http;
@@ -2,22 +2,55 @@
2
2
  <div class="test">
3
3
  <h1>this is test page</h1>
4
4
  <p>counter: {{ counter.count }}</p>
5
- <el-button type="primary" @click="handleClick"> go to home page </el-button>
6
- <el-button type="success" @click="counter.inc"> +1 </el-button>
5
+ <button id="test-button" @click="handleClick">go to home page</button>
6
+ <button id="test-button-inc" @click="counter.inc">+1</button>
7
7
  </div>
8
8
  </template>
9
9
  <script setup lang="ts">
10
10
  import { useAlert } from '@composables/useAlert';
11
11
  import { useCounterStore } from '@stores/useCounter';
12
- import { fetchDemoUser } from '@api/modules/demo';
13
12
  const alert = useAlert();
14
13
  const counter = useCounterStore();
15
14
 
16
15
  const handleClick = () => {
17
16
  alert('测试alert', 'error');
18
17
  };
19
-
20
- fetchDemoUser().catch((): void => undefined);
21
18
  </script>
22
19
 
23
- <style scoped></style>
20
+ <style scoped>
21
+ .test {
22
+ display: flex;
23
+ flex-direction: column;
24
+ align-items: center;
25
+ justify-content: center;
26
+ height: 100vh;
27
+ }
28
+
29
+ #test-button {
30
+ margin-top: 20px;
31
+ padding: 10px 20px;
32
+ border-radius: 5px;
33
+ border: 1px solid #ccc;
34
+ background-color: #f0f0f0;
35
+ color: #333;
36
+ cursor: pointer;
37
+ }
38
+
39
+ #test-button:hover {
40
+ background-color: #e0e0e0;
41
+ }
42
+
43
+ #test-button-inc {
44
+ margin-top: 20px;
45
+ padding: 10px 20px;
46
+ border-radius: 5px;
47
+ border: 1px solid #ccc;
48
+ background-color: #f0f0f0;
49
+ color: #333;
50
+ cursor: pointer;
51
+ }
52
+
53
+ #test-button-inc:hover {
54
+ background-color: #e0e0e0;
55
+ }
56
+ </style>
@@ -1,75 +1,68 @@
1
- import { defineConfig } from 'vite';
1
+ import { defineConfig, loadEnv } from 'vite';
2
2
  import vue from '@vitejs/plugin-vue';
3
3
  import AutoImport from 'unplugin-auto-import/vite';
4
4
  import Components from 'unplugin-vue-components/vite';
5
- import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
6
5
  import copyPlugin from 'rollup-plugin-copy';
7
6
  import path from 'path';
8
- import postCssPxToRem from 'postcss-pxtorem';
7
+ import autoprefixer from 'autoprefixer';
9
8
 
10
- const rootValue = 192;
9
+ export default defineConfig(({ mode }) => {
10
+ const env = loadEnv(mode, process.cwd(), '');
11
11
 
12
- export default defineConfig({
13
- base: './',
14
- build: {
15
- sourcemap: true,
16
- emptyOutDir: false,
17
- rollupOptions: {
18
- plugins: [
19
- copyPlugin({
20
- targets: [{ src: 'src/assets/**/*', dest: 'dist/assets' }],
21
- }),
22
- ],
12
+ return {
13
+ base: './',
14
+ build: {
15
+ sourcemap: true,
16
+ emptyOutDir: false,
17
+ rollupOptions: {
18
+ plugins: [
19
+ copyPlugin({
20
+ targets: [{ src: 'src/assets/**/*', dest: 'dist/assets' }],
21
+ }),
22
+ ],
23
+ },
24
+ outDir: 'dist',
25
+ assetsDir: 'assets',
23
26
  },
24
- outDir: 'dist',
25
- assetsDir: 'assets',
26
- },
27
- plugins: [
28
- vue(),
29
- AutoImport({
30
- resolvers: [ElementPlusResolver()],
31
- }),
32
- Components({
33
- resolvers: [ElementPlusResolver()],
34
- }),
35
- ],
36
- resolve: {
37
- alias: {
38
- '@': path.resolve(__dirname, './src'),
39
- '@assets': path.resolve(__dirname, './src/assets'),
40
- '@style': path.resolve(__dirname, './src/styles'),
41
- '@stores': path.resolve(__dirname, './src/stores'),
42
- '@components': path.resolve(__dirname, './src/components'),
43
- '@utils': path.resolve(__dirname, './src/utils'),
44
- '@api': path.resolve(__dirname, './src/api'),
45
- '@types': path.resolve(__dirname, './types'),
46
- '@views': path.resolve(__dirname, './src/views'),
47
- '@composables': path.resolve(__dirname, './src/utils/composables'),
48
- '@router': path.resolve(__dirname, './src/router'),
49
- '@directive': path.resolve(__dirname, './src/directive'),
50
- '@layout': path.resolve(__dirname, './src/layout'),
51
- '@interface': path.resolve(__dirname, './interface'),
27
+ plugins: [vue(), AutoImport({}), Components({})],
28
+ resolve: {
29
+ alias: {
30
+ '@': path.resolve(__dirname, './src'),
31
+ '@assets': path.resolve(__dirname, './src/assets'),
32
+ '@style': path.resolve(__dirname, './src/styles'),
33
+ '@stores': path.resolve(__dirname, './src/stores'),
34
+ '@components': path.resolve(__dirname, './src/components'),
35
+ '@utils': path.resolve(__dirname, './src/utils'),
36
+ '@api': path.resolve(__dirname, './src/api'),
37
+ '@types': path.resolve(__dirname, './types'),
38
+ '@views': path.resolve(__dirname, './src/views'),
39
+ '@composables': path.resolve(__dirname, './src/utils/composables'),
40
+ '@router': path.resolve(__dirname, './src/router'),
41
+ '@directive': path.resolve(__dirname, './src/directive'),
42
+ '@layout': path.resolve(__dirname, './src/layout'),
43
+ '@interface': path.resolve(__dirname, './interface'),
44
+ },
52
45
  },
53
- },
54
- css: {
55
- preprocessorOptions: {
56
- less: {
57
- javascriptEnabled: true,
46
+ css: {
47
+ preprocessorOptions: {
48
+ less: {
49
+ javascriptEnabled: true,
50
+ },
51
+ },
52
+ postcss: {
53
+ plugins: [autoprefixer()],
58
54
  },
59
55
  },
60
- postcss: {
61
- plugins: [
62
- postCssPxToRem({
63
- rootValue: rootValue,
64
- propList: ['*'],
65
- selectorBlackList: ['.norem'],
66
- }),
67
- ],
56
+ server: {
57
+ host: '0.0.0.0',
58
+ port: 3000,
59
+ open: true,
60
+ proxy: {
61
+ '/demo': {
62
+ target: env.VITE_DEMO_PROXY_TARGET,
63
+ changeOrigin: true,
64
+ },
65
+ },
68
66
  },
69
- },
70
- server: {
71
- host: '0.0.0.0',
72
- port: 3000,
73
- open: true,
74
- },
67
+ };
75
68
  });