vue2server7 7.0.50 → 7.0.52
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/frontEnd/.workbuddy/memory/MEMORY.md +0 -0
- package/frontEnd/src/components/MoneyInput.vue +262 -0
- package/frontEnd/src/pages/MoneyInputPage.vue +101 -0
- package/frontEnd/src/router/routes.js +10 -0
- package/package.json +1 -1
- package/ms-vscode-remote.remote-ssh-0.119.2025033120.vsix +0 -0
- package/test/111 +0 -52
- package/test/111.txt +0 -44
- package/test/11111111111 +0 -24
- package/test/111111111111111112222 +0 -60
- package/test/12.js +0 -115
- package/test/13420256837985870.gif +0 -0
- package/test/13420256921603132.gif +0 -0
- package/test/13420256985163546.png +0 -0
- package/test/222.txt +0 -47
- package/test/3.js +0 -75
- package/test/4.js +0 -60
- package/test/6.js +0 -60
- package/test/777 +0 -24
- package/test/9.text +0 -100
- package/test/convert-java-to-ts.js +0 -52
- package/test/fun.js +0 -15
- package/test/health.test.js +0 -12
- package/test/i.js +0 -17
- package/test/trend.test.js +0 -30
- package/test/vue3_permission_directive.md +0 -206
- package/test/vue3_permission_directive_advanced.md +0 -248
package/test/222.txt
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { defineStore } from "pinia"
|
|
2
|
-
import { ref, computed } from "vue"
|
|
3
|
-
import { CharRefSheet } from "@chic/types/constant"
|
|
4
|
-
import storage from "@chic/utils/storage"
|
|
5
|
-
|
|
6
|
-
export const useCBCManualVerificationStore = defineStore(
|
|
7
|
-
"CBCManualVerification",
|
|
8
|
-
() => {
|
|
9
|
-
// state
|
|
10
|
-
const selectList = ref<any[]>([])
|
|
11
|
-
|
|
12
|
-
// getter(获取)
|
|
13
|
-
const getSelectList = computed(() => selectList.value)
|
|
14
|
-
|
|
15
|
-
// actions
|
|
16
|
-
const setSelectList = (list: any[]) => {
|
|
17
|
-
selectList.value = list
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const addSelectItem = (item: any) => {
|
|
21
|
-
selectList.value.push(item)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const clearSelectList = () => {
|
|
25
|
-
selectList.value = []
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
selectList,
|
|
30
|
-
getSelectList,
|
|
31
|
-
setSelectList,
|
|
32
|
-
addSelectItem,
|
|
33
|
-
clearSelectList
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
persist: {
|
|
38
|
-
enabled: true,
|
|
39
|
-
strategies: [
|
|
40
|
-
{
|
|
41
|
-
key: CharRefSheet.k101y,
|
|
42
|
-
storage
|
|
43
|
-
}
|
|
44
|
-
]
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
)
|
package/test/3.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
|
|
3
|
-
const content = fs.readFileSync("input.txt", "utf8");
|
|
4
|
-
|
|
5
|
-
// Java → TS 类型
|
|
6
|
-
function javaToTsType(javaType) {
|
|
7
|
-
const map = {
|
|
8
|
-
String: "string",
|
|
9
|
-
Integer: "number",
|
|
10
|
-
Long: "number",
|
|
11
|
-
Double: "number",
|
|
12
|
-
Float: "number",
|
|
13
|
-
BigDecimal: "number",
|
|
14
|
-
Boolean: "boolean",
|
|
15
|
-
Date: "string",
|
|
16
|
-
LocalDate: "string",
|
|
17
|
-
LocalDateTime: "string"
|
|
18
|
-
};
|
|
19
|
-
return map[javaType] || "any";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// 默认值
|
|
23
|
-
function defaultValue(tsType) {
|
|
24
|
-
switch (tsType) {
|
|
25
|
-
case "string":
|
|
26
|
-
return `""`;
|
|
27
|
-
case "number":
|
|
28
|
-
return `0`;
|
|
29
|
-
case "boolean":
|
|
30
|
-
return `false`;
|
|
31
|
-
default:
|
|
32
|
-
return `null`;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const regex =
|
|
37
|
-
/@ApiModelProperty\s*\(\s*value\s*=\s*"([^"]+)"[^)]*\)\s*[\r\n]+\s*private\s+(\w+)\s+(\w+)\s*;/g;
|
|
38
|
-
|
|
39
|
-
let match;
|
|
40
|
-
|
|
41
|
-
const jsonResult = [];
|
|
42
|
-
const tsFields = [];
|
|
43
|
-
const formFields = [];
|
|
44
|
-
|
|
45
|
-
while ((match = regex.exec(content)) !== null) {
|
|
46
|
-
const label = match[1];
|
|
47
|
-
const javaType = match[2];
|
|
48
|
-
const field = match[3];
|
|
49
|
-
|
|
50
|
-
const tsType = javaToTsType(javaType);
|
|
51
|
-
|
|
52
|
-
// JSON
|
|
53
|
-
jsonResult.push({ field, label });
|
|
54
|
-
|
|
55
|
-
// TS
|
|
56
|
-
tsFields.push(` /** ${label} */\n ${field}: ${tsType};`);
|
|
57
|
-
|
|
58
|
-
// JS 表单对象(带注释)
|
|
59
|
-
formFields.push(` /** ${label} */\n ${field}: ${defaultValue(tsType)}`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 写文件
|
|
63
|
-
fs.writeFileSync("output.json", JSON.stringify(jsonResult, null, 2));
|
|
64
|
-
|
|
65
|
-
fs.writeFileSync(
|
|
66
|
-
"model.ts",
|
|
67
|
-
`export interface Model {\n${tsFields.join("\n\n")}\n}\n`
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
fs.writeFileSync(
|
|
71
|
-
"form.js",
|
|
72
|
-
`export const formModel = {\n${formFields.join(",\n\n")}\n};\n`
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
console.log("✅ 已生成 output.json / model.ts / form.js");
|
package/test/4.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
const express = require('express');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
|
|
5
|
-
const app = express();
|
|
6
|
-
const PORT = 3000;
|
|
7
|
-
|
|
8
|
-
// 设置 public 为静态目录
|
|
9
|
-
const publicDir = path.join(__dirname, 'public');
|
|
10
|
-
app.use('/static', express.static(publicDir));
|
|
11
|
-
|
|
12
|
-
// 首页:列出 public 文件夹里的所有文件
|
|
13
|
-
app.get('/', (req, res) => {
|
|
14
|
-
fs.readdir(publicDir, (err, files) => {
|
|
15
|
-
if (err) {
|
|
16
|
-
return res.status(500).send('读取文件夹失败');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
let fileListHtml = `
|
|
20
|
-
<h1>文件列表</h1>
|
|
21
|
-
<ul>
|
|
22
|
-
`;
|
|
23
|
-
|
|
24
|
-
files.forEach(file => {
|
|
25
|
-
fileListHtml += `
|
|
26
|
-
<li>
|
|
27
|
-
${file} -
|
|
28
|
-
<a href="/download/${file}">下载</a>
|
|
29
|
-
</li>
|
|
30
|
-
`;
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
fileListHtml += `
|
|
34
|
-
</ul>
|
|
35
|
-
`;
|
|
36
|
-
|
|
37
|
-
res.send(fileListHtml);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// 下载接口
|
|
42
|
-
app.get('/download/:filename', (req, res) => {
|
|
43
|
-
const filename = req.params.filename;
|
|
44
|
-
const filePath = path.join(publicDir, filename);
|
|
45
|
-
|
|
46
|
-
// 防止路径穿越攻击
|
|
47
|
-
if (!filePath.startsWith(publicDir)) {
|
|
48
|
-
return res.status(400).send('非法访问');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
res.download(filePath, filename, (err) => {
|
|
52
|
-
if (err) {
|
|
53
|
-
res.status(404).send('文件不存在');
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
app.listen(PORT, () => {
|
|
59
|
-
console.log(`服务器已启动: http://localhost:${PORT}`);
|
|
60
|
-
});
|
package/test/6.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
const express = require('express');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
|
|
5
|
-
const app = express();
|
|
6
|
-
const PORT = 3000;
|
|
7
|
-
|
|
8
|
-
// 设置 public 为静态目录
|
|
9
|
-
const publicDir = path.join(__dirname, 'public');
|
|
10
|
-
app.use('/static', express.static(publicDir));
|
|
11
|
-
|
|
12
|
-
// 首页:列出 public 文件夹里的所有文件
|
|
13
|
-
app.get('/', (req, res) => {
|
|
14
|
-
fs.readdir(publicDir, (err, files) => {
|
|
15
|
-
if (err) {
|
|
16
|
-
return res.status(500).send('读取文件夹失败');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
let fileListHtml = `
|
|
20
|
-
<h1>文件列表</h1>
|
|
21
|
-
<ul>
|
|
22
|
-
`;
|
|
23
|
-
|
|
24
|
-
files.forEach(file => {
|
|
25
|
-
fileListHtml += `
|
|
26
|
-
<li>
|
|
27
|
-
${file} -
|
|
28
|
-
<a href="/download/${file}">下载</a>
|
|
29
|
-
</li>
|
|
30
|
-
`;
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
fileListHtml += `
|
|
34
|
-
</ul>
|
|
35
|
-
`;
|
|
36
|
-
|
|
37
|
-
res.send(fileListHtml);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// 下载接口
|
|
42
|
-
app.get('/download/:filename', (req, res) => {
|
|
43
|
-
const filename = req.params.filename;
|
|
44
|
-
const filePath = path.join(publicDir, filename);
|
|
45
|
-
|
|
46
|
-
// 防止路径穿越攻击
|
|
47
|
-
if (!filePath.startsWith(publicDir)) {
|
|
48
|
-
return res.status(400).send('非法访问');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
res.download(filePath, filename, (err) => {
|
|
52
|
-
if (err) {
|
|
53
|
-
res.status(404).send('文件不存在');
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
app.listen(PORT, () => {
|
|
59
|
-
console.log(`服务器已启动: http://localhost:${PORT}`);
|
|
60
|
-
});
|
package/test/777
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import axios from 'axios'
|
|
2
|
-
|
|
3
|
-
const downloadFile = async (url: string, filename = 'file.zip') => {
|
|
4
|
-
const res = await axios.get(url, {
|
|
5
|
-
responseType: 'blob',
|
|
6
|
-
onDownloadProgress: (e) => {
|
|
7
|
-
if (e.total) {
|
|
8
|
-
const percent = Math.round((e.loaded / e.total) * 100)
|
|
9
|
-
console.log('下载进度:', percent + '%')
|
|
10
|
-
} else {
|
|
11
|
-
console.log('已下载:', e.loaded)
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
// 创建下载
|
|
17
|
-
const blob = new Blob([res.data])
|
|
18
|
-
const link = document.createElement('a')
|
|
19
|
-
link.href = URL.createObjectURL(blob)
|
|
20
|
-
link.download = filename
|
|
21
|
-
link.click()
|
|
22
|
-
|
|
23
|
-
URL.revokeObjectURL(link.href)
|
|
24
|
-
}
|
package/test/9.text
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
|
|
3
|
-
const content = fs.readFileSync("input.txt", "utf8");
|
|
4
|
-
const lines = content.split(/\r?\n/);
|
|
5
|
-
|
|
6
|
-
// Java → TS 类型
|
|
7
|
-
function javaToTsType(javaType) {
|
|
8
|
-
const map = {
|
|
9
|
-
String: "string",
|
|
10
|
-
Integer: "number",
|
|
11
|
-
Long: "number",
|
|
12
|
-
Double: "number",
|
|
13
|
-
Float: "number",
|
|
14
|
-
BigDecimal: "number",
|
|
15
|
-
Boolean: "boolean",
|
|
16
|
-
Date: "string",
|
|
17
|
-
LocalDate: "string",
|
|
18
|
-
LocalDateTime: "string",
|
|
19
|
-
};
|
|
20
|
-
return map[javaType] || "any";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// TS 默认值
|
|
24
|
-
function defaultValue(tsType) {
|
|
25
|
-
switch (tsType) {
|
|
26
|
-
case "string":
|
|
27
|
-
return `""`;
|
|
28
|
-
case "number":
|
|
29
|
-
return `0`;
|
|
30
|
-
case "boolean":
|
|
31
|
-
return `false`;
|
|
32
|
-
default:
|
|
33
|
-
return `null`;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// 支持:@ApiModelProperty(value="xx") / @ApiModelProperty(value = "xx", required=true)
|
|
38
|
-
// 也顺手兼容:@ApiModelProperty("xx") 这种写法(有些项目会这样写)
|
|
39
|
-
function parseApiModelProperty(line) {
|
|
40
|
-
let m = /@ApiModelProperty\s*\(\s*value\s*=\s*"([^"]+)"/.exec(line);
|
|
41
|
-
if (m?.[1]) return m[1];
|
|
42
|
-
m = /@ApiModelProperty\s*\(\s*"([^"]+)"\s*\)/.exec(line);
|
|
43
|
-
if (m?.[1]) return m[1];
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// 支持:@CheckV(desc="xx") / @CheckV(desc = "xx", ...)
|
|
48
|
-
function parseCheckV(line) {
|
|
49
|
-
const m = /@CheckV\s*\(\s*desc\s*=\s*"([^"]+)"/.exec(line);
|
|
50
|
-
return m?.[1] || null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// 字段:private String queryDate;
|
|
54
|
-
function parseField(line) {
|
|
55
|
-
const m = /^\s*private\s+(\w+)\s+(\w+)\s*;\s*$/.exec(line);
|
|
56
|
-
if (!m) return null;
|
|
57
|
-
return { javaType: m[1], field: m[2] };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
let apiLabel = null;
|
|
61
|
-
let checkLabel = null;
|
|
62
|
-
|
|
63
|
-
const jsonResult = [];
|
|
64
|
-
const tsFields = [];
|
|
65
|
-
const formFields = [];
|
|
66
|
-
|
|
67
|
-
for (const line of lines) {
|
|
68
|
-
const a = parseApiModelProperty(line);
|
|
69
|
-
if (a) {
|
|
70
|
-
apiLabel = a;
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const c = parseCheckV(line);
|
|
75
|
-
if (c) {
|
|
76
|
-
checkLabel = c;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const f = parseField(line);
|
|
81
|
-
if (f) {
|
|
82
|
-
const label = apiLabel || checkLabel || f.field;
|
|
83
|
-
const tsType = javaToTsType(f.javaType);
|
|
84
|
-
|
|
85
|
-
jsonResult.push({ field: f.field, label });
|
|
86
|
-
tsFields.push(` /** ${label} */\n ${f.field}: ${tsType};`);
|
|
87
|
-
formFields.push(` /** ${label} */\n ${f.field}: ${defaultValue(tsType)}`);
|
|
88
|
-
|
|
89
|
-
// ✅ 关键:输出后立刻清空,避免串到下一个字段
|
|
90
|
-
apiLabel = null;
|
|
91
|
-
checkLabel = null;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// 写文件
|
|
96
|
-
fs.writeFileSync("output.json", JSON.stringify(jsonResult, null, 2), "utf8");
|
|
97
|
-
fs.writeFileSync("model.ts", `export interface Model {\n${tsFields.join("\n\n")}\n}\n`, "utf8");
|
|
98
|
-
fs.writeFileSync("form.js", `export const formModel = {\n${formFields.join(",\n\n")}\n};\n`, "utf8");
|
|
99
|
-
|
|
100
|
-
console.log("✅ 已生成 output.json / model.ts / form.js");
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
|
|
3
|
-
const content = fs.readFileSync("input.txt", "utf8");
|
|
4
|
-
|
|
5
|
-
// Java 类型 → TS 类型映射
|
|
6
|
-
function javaToTsType(javaType) {
|
|
7
|
-
const map = {
|
|
8
|
-
String: "string",
|
|
9
|
-
Integer: "number",
|
|
10
|
-
Long: "number",
|
|
11
|
-
Double: "number",
|
|
12
|
-
Float: "number",
|
|
13
|
-
BigDecimal: "number",
|
|
14
|
-
Boolean: "boolean",
|
|
15
|
-
Date: "string",
|
|
16
|
-
LocalDate: "string",
|
|
17
|
-
LocalDateTime: "string"
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
return map[javaType] || "any";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// 正则匹配 注解 + 字段
|
|
24
|
-
const regex =
|
|
25
|
-
/@ApiModelProperty\s*\(\s*value\s*=\s*"([^"]+)"[^)]*\)\s*[\r\n]+\s*private\s+(\w+)\s+(\w+)\s*;/g;
|
|
26
|
-
|
|
27
|
-
let match;
|
|
28
|
-
const jsonResult = [];
|
|
29
|
-
const tsFields = [];
|
|
30
|
-
|
|
31
|
-
while ((match = regex.exec(content)) !== null) {
|
|
32
|
-
const label = match[1]; // 中文
|
|
33
|
-
const javaType = match[2]; // Java类型
|
|
34
|
-
const field = match[3]; // 字段名
|
|
35
|
-
const tsType = javaToTsType(javaType);
|
|
36
|
-
|
|
37
|
-
jsonResult.push({
|
|
38
|
-
field,
|
|
39
|
-
label
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
tsFields.push(` /** ${label} */\n ${field}: ${tsType};`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// 输出 JSON 文件
|
|
46
|
-
fs.writeFileSync("output.json", JSON.stringify(jsonResult, null, 2));
|
|
47
|
-
|
|
48
|
-
// 输出 TS 文件
|
|
49
|
-
const tsContent = `export interface Model {\n${tsFields.join("\n\n")}\n}\n`;
|
|
50
|
-
fs.writeFileSync("model.ts", tsContent);
|
|
51
|
-
|
|
52
|
-
console.log("✅ 已生成 output.json 和 model.ts");
|
package/test/fun.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
function addIdText(list = []) {
|
|
2
|
-
const stack = [...list]
|
|
3
|
-
|
|
4
|
-
while (stack.length) {
|
|
5
|
-
const node = stack.pop()
|
|
6
|
-
|
|
7
|
-
node.idText = `${node.id}-${node.text}`
|
|
8
|
-
|
|
9
|
-
if (Array.isArray(node.children) && node.children.length) {
|
|
10
|
-
stack.push(...node.children)
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return list
|
|
15
|
-
}
|
package/test/health.test.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
const assert = require("node:assert/strict");
|
|
2
|
-
const test = require("node:test");
|
|
3
|
-
|
|
4
|
-
const request = require("supertest");
|
|
5
|
-
|
|
6
|
-
const app = require("../dist/app").default;
|
|
7
|
-
|
|
8
|
-
test("GET /health returns ok", async () => {
|
|
9
|
-
const res = await request(app).get("/api/health").expect(200);
|
|
10
|
-
assert.equal(res.body.code, 0);
|
|
11
|
-
assert.equal(res.body.data.status, "ok");
|
|
12
|
-
});
|
package/test/i.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'child_process'
|
|
2
|
-
|
|
3
|
-
export function hasWebStorm() {
|
|
4
|
-
try {
|
|
5
|
-
const result = execSync(
|
|
6
|
-
`mdfind "kMDItemCFBundleIdentifier == 'com.jetbrains.WebStorm'"`,
|
|
7
|
-
{ encoding: 'utf-8' }
|
|
8
|
-
).trim()
|
|
9
|
-
|
|
10
|
-
return result.length > 0
|
|
11
|
-
} catch (e) {
|
|
12
|
-
return false
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// 测试
|
|
17
|
-
console.log('WebStorm 是否安装:', hasWebStorm())
|
package/test/trend.test.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
const assert = require("node:assert/strict");
|
|
2
|
-
const test = require("node:test");
|
|
3
|
-
const request = require("supertest");
|
|
4
|
-
|
|
5
|
-
const app = require("../dist/app").default;
|
|
6
|
-
|
|
7
|
-
function isLeapYear(year) {
|
|
8
|
-
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
test("POST /api/mock/trend returns full year daily data", async () => {
|
|
12
|
-
const year = 2024;
|
|
13
|
-
const res = await request(app).post("/api/mock/trend").send({ year }).expect(200);
|
|
14
|
-
assert.equal(res.body.code, 0);
|
|
15
|
-
const data = res.body.data;
|
|
16
|
-
assert.ok(Array.isArray(data), "data should be an array");
|
|
17
|
-
assert.equal(data[0].label, `${year}-01-01`);
|
|
18
|
-
assert.equal(data[data.length - 1].label, `${year}-12-31`);
|
|
19
|
-
assert.equal(data.length, isLeapYear(year) ? 366 : 365);
|
|
20
|
-
// Check that all values are either number or "_"
|
|
21
|
-
for (const item of data) {
|
|
22
|
-
assert.ok(typeof item.value === "number" || item.value === "_", `Value ${item.value} at ${item.label} is invalid`);
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("POST /api/mock/trend rejects invalid year", async () => {
|
|
27
|
-
const res = await request(app).post("/api/mock/trend").send({ year: "abc" }).expect(400);
|
|
28
|
-
assert.equal(res.body.code, "INVALID_YEAR");
|
|
29
|
-
});
|
|
30
|
-
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
# Vue3 按钮权限控制(自定义指令 + Pinia 版)
|
|
2
|
-
|
|
3
|
-
## 📌 目标
|
|
4
|
-
|
|
5
|
-
实现一套企业级按钮权限控制方案:
|
|
6
|
-
|
|
7
|
-
- ✔ 从 Pinia 获取权限
|
|
8
|
-
- ✔ 支持单权限 / 多权限
|
|
9
|
-
- ✔ 支持 AND / OR 逻辑
|
|
10
|
-
- ✔ 支持动态更新权限
|
|
11
|
-
- ✔ 全局指令 v-permission
|
|
12
|
-
- ✔ 自动响应权限变化
|
|
13
|
-
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
# 🧠 一、权限 Store(Pinia)
|
|
17
|
-
|
|
18
|
-
```ts
|
|
19
|
-
// stores/auth.ts
|
|
20
|
-
import { defineStore } from 'pinia'
|
|
21
|
-
|
|
22
|
-
export const useAuthStore = defineStore('auth', {
|
|
23
|
-
state: () => ({
|
|
24
|
-
permissions: [] as string[],
|
|
25
|
-
}),
|
|
26
|
-
|
|
27
|
-
actions: {
|
|
28
|
-
setPermissions(perms: string[]) {
|
|
29
|
-
this.permissions = perms
|
|
30
|
-
},
|
|
31
|
-
|
|
32
|
-
clearPermissions() {
|
|
33
|
-
this.permissions = []
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
})
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
# 🧩 二、权限判断工具函数
|
|
42
|
-
|
|
43
|
-
```ts
|
|
44
|
-
// utils/permission.ts
|
|
45
|
-
|
|
46
|
-
export type PermissionValue =
|
|
47
|
-
| string
|
|
48
|
-
| string[]
|
|
49
|
-
| { value: string[]; mode?: 'and' | 'or' }
|
|
50
|
-
|
|
51
|
-
export function hasPermission(
|
|
52
|
-
userPermissions: string[],
|
|
53
|
-
required?: PermissionValue
|
|
54
|
-
): boolean {
|
|
55
|
-
if (!required) return true
|
|
56
|
-
|
|
57
|
-
if (typeof required === 'string') {
|
|
58
|
-
return userPermissions.includes(required)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (Array.isArray(required)) {
|
|
62
|
-
return required.some(p => userPermissions.includes(p))
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const { value, mode = 'or' } = required
|
|
66
|
-
|
|
67
|
-
if (mode === 'and') {
|
|
68
|
-
return value.every(p => userPermissions.includes(p))
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return value.some(p => userPermissions.includes(p))
|
|
72
|
-
}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
# ⚙️ 三、自定义指令 v-permission
|
|
78
|
-
|
|
79
|
-
```ts
|
|
80
|
-
// directives/permission.ts
|
|
81
|
-
import type { Directive } from 'vue'
|
|
82
|
-
import { useAuthStore } from '@/stores/auth'
|
|
83
|
-
import { hasPermission } from '@/utils/permission'
|
|
84
|
-
|
|
85
|
-
export const permission: Directive = {
|
|
86
|
-
mounted(el, binding) {
|
|
87
|
-
const authStore = useAuthStore()
|
|
88
|
-
|
|
89
|
-
const check = () => {
|
|
90
|
-
const ok = hasPermission(authStore.permissions, binding.value)
|
|
91
|
-
|
|
92
|
-
if (!ok) {
|
|
93
|
-
el.remove()
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
check()
|
|
98
|
-
},
|
|
99
|
-
|
|
100
|
-
updated(el, binding) {
|
|
101
|
-
const authStore = useAuthStore()
|
|
102
|
-
|
|
103
|
-
const ok = hasPermission(authStore.permissions, binding.value)
|
|
104
|
-
|
|
105
|
-
if (!ok) {
|
|
106
|
-
el.remove()
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
---
|
|
113
|
-
|
|
114
|
-
# 🚀 四、全局注册指令
|
|
115
|
-
|
|
116
|
-
```ts
|
|
117
|
-
// main.ts
|
|
118
|
-
import { createApp } from 'vue'
|
|
119
|
-
import App from './App.vue'
|
|
120
|
-
import { createPinia } from 'pinia'
|
|
121
|
-
import { permission } from '@/directives/permission'
|
|
122
|
-
|
|
123
|
-
const app = createApp(App)
|
|
124
|
-
|
|
125
|
-
app.use(createPinia())
|
|
126
|
-
|
|
127
|
-
app.directive('permission', permission)
|
|
128
|
-
|
|
129
|
-
app.mount('#app')
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
---
|
|
133
|
-
|
|
134
|
-
# 🧪 五、使用方式
|
|
135
|
-
|
|
136
|
-
## ✔ 单权限
|
|
137
|
-
|
|
138
|
-
```vue
|
|
139
|
-
<button v-permission="'user:add'">
|
|
140
|
-
新增用户
|
|
141
|
-
</button>
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
---
|
|
145
|
-
|
|
146
|
-
## ✔ 多权限(OR)
|
|
147
|
-
|
|
148
|
-
```vue
|
|
149
|
-
<button v-permission="['user:add', 'user:edit']">
|
|
150
|
-
新增 / 编辑
|
|
151
|
-
</button>
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
---
|
|
155
|
-
|
|
156
|
-
## ✔ AND 权限
|
|
157
|
-
|
|
158
|
-
```vue
|
|
159
|
-
<button
|
|
160
|
-
v-permission="{
|
|
161
|
-
value: ['user:add', 'user:delete'],
|
|
162
|
-
mode: 'and'
|
|
163
|
-
}"
|
|
164
|
-
>
|
|
165
|
-
必须同时拥有权限
|
|
166
|
-
</button>
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
---
|
|
170
|
-
|
|
171
|
-
# ⚠️ 六、常见问题
|
|
172
|
-
|
|
173
|
-
## ❌ Pinia 未初始化
|
|
174
|
-
|
|
175
|
-
```ts
|
|
176
|
-
let authStore: any
|
|
177
|
-
|
|
178
|
-
function getStore() {
|
|
179
|
-
if (!authStore) {
|
|
180
|
-
authStore = useAuthStore()
|
|
181
|
-
}
|
|
182
|
-
return authStore
|
|
183
|
-
}
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
---
|
|
187
|
-
|
|
188
|
-
## ❌ 权限不更新
|
|
189
|
-
|
|
190
|
-
```ts
|
|
191
|
-
import { watch } from 'vue'
|
|
192
|
-
|
|
193
|
-
watch(
|
|
194
|
-
() => authStore.permissions,
|
|
195
|
-
() => {
|
|
196
|
-
// recheck
|
|
197
|
-
},
|
|
198
|
-
{ deep: true }
|
|
199
|
-
)
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
---
|
|
203
|
-
|
|
204
|
-
# 🎯 总结
|
|
205
|
-
|
|
206
|
-
适用于中后台系统的权限控制方案,支持扩展 RBAC / 路由权限 / 菜单权限。
|