移除测试文件
This commit is contained in:
56
cdTMP/package-lock.json
generated
56
cdTMP/package-lock.json
generated
@@ -32,7 +32,7 @@
|
||||
"vue": "^3.5.25",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"vue-color-kit": "^1.0.6",
|
||||
"vue-data-ui": "^3.7.14",
|
||||
"vue-data-ui": "^3.7.15",
|
||||
"vue-router": "^4.6.3",
|
||||
"vuedraggable": "^2.24.3"
|
||||
},
|
||||
@@ -46,13 +46,13 @@
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.2",
|
||||
"@vue/babel-plugin-jsx": "^2.0.1",
|
||||
"browserslist": "^4.28.0",
|
||||
"browserslist": "^4.28.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-vue": "^10.6.2",
|
||||
"less": "^4.4.2",
|
||||
"less-loader": "^12.3.0",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.7.3",
|
||||
"prettier": "^3.7.4",
|
||||
"rollup-plugin-visualizer": "^6.0.5",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"typescript": "^5.9.3",
|
||||
@@ -2338,9 +2338,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.28",
|
||||
"resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz",
|
||||
"integrity": "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==",
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.0.tgz",
|
||||
"integrity": "sha512-Mh++g+2LPfzZToywfE1BUzvZbfOY52Nil0rn9H1CPC5DJ7fX+Vir7nToBeoiSbB1zTNeGYbELEvJESujgGrzXw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -2381,9 +2381,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.0.tgz",
|
||||
"integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==",
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz",
|
||||
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2402,11 +2402,11 @@
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.25",
|
||||
"caniuse-lite": "^1.0.30001754",
|
||||
"electron-to-chromium": "^1.5.249",
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
"electron-to-chromium": "^1.5.263",
|
||||
"node-releases": "^2.0.27",
|
||||
"update-browserslist-db": "^1.1.4"
|
||||
"update-browserslist-db": "^1.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
@@ -2455,9 +2455,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001755",
|
||||
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz",
|
||||
"integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==",
|
||||
"version": "1.0.30001759",
|
||||
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz",
|
||||
"integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2755,9 +2755,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.254",
|
||||
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz",
|
||||
"integrity": "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg==",
|
||||
"version": "1.5.264",
|
||||
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.264.tgz",
|
||||
"integrity": "sha512-1tEf0nLgltC3iy9wtlYDlQDc5Rg9lEKVjEmIHJ21rI9OcqkvD45K1oyNIRA4rR1z3LgJ7KeGzEBojVcV6m4qjA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -4622,9 +4622,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.7.3",
|
||||
"resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.7.3.tgz",
|
||||
"integrity": "sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==",
|
||||
"version": "3.7.4",
|
||||
"resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.7.4.tgz",
|
||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -5299,9 +5299,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
|
||||
"integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.1.tgz",
|
||||
"integrity": "sha512-R9NcHbbZ45RoWfTdhn1J9SS7zxNvlddv4YRrHTUaFdtjbmfncfedB45EC9IaqJQ97iAR1GZgOfyRQO+ExIF6EQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -5462,9 +5462,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue-data-ui": {
|
||||
"version": "3.7.14",
|
||||
"resolved": "https://registry.npmmirror.com/vue-data-ui/-/vue-data-ui-3.7.14.tgz",
|
||||
"integrity": "sha512-MhEd3ZN0w1VVAnJPwo1xCcx1bg8owGm3+4Apn/kxO/dh+k3NqOXdGjaRrKOZdqc63l1GQ03hrAVknYxpILo9hA==",
|
||||
"version": "3.7.15",
|
||||
"resolved": "https://registry.npmmirror.com/vue-data-ui/-/vue-data-ui-3.7.15.tgz",
|
||||
"integrity": "sha512-JGYwU4tWiRC9FmIsApaCOHgmVqAMMJOihaThqVcqwEpUyGOdzH0fkWyQcZcovEqWXzL5YnziG/9ncMoYN784ng==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"jspdf": "^3.0.1",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"vue": "^3.5.25",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"vue-color-kit": "^1.0.6",
|
||||
"vue-data-ui": "^3.7.14",
|
||||
"vue-data-ui": "^3.7.15",
|
||||
"vue-router": "^4.6.3",
|
||||
"vuedraggable": "^2.24.3"
|
||||
},
|
||||
@@ -49,13 +49,13 @@
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.2",
|
||||
"@vue/babel-plugin-jsx": "^2.0.1",
|
||||
"browserslist": "^4.28.0",
|
||||
"browserslist": "^4.28.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-vue": "^10.6.2",
|
||||
"less": "^4.4.2",
|
||||
"less-loader": "^12.3.0",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.7.3",
|
||||
"prettier": "^3.7.4",
|
||||
"rollup-plugin-visualizer": "^6.0.5",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
@@ -13,6 +13,14 @@ export default {
|
||||
* @returns 可流式或一次性
|
||||
*/
|
||||
getAiTestItem(data: DataRowType) {
|
||||
if (import.meta.env.DEV) {
|
||||
return request({
|
||||
url: `/local_doc_qa/testing_item`,
|
||||
timeout: 20000,
|
||||
method: "post",
|
||||
data
|
||||
})
|
||||
}
|
||||
return request({
|
||||
url: `${AI_API_BASE}/api/local_doc_qa/testing_item`,
|
||||
timeout: 20000,
|
||||
|
||||
@@ -33,7 +33,7 @@ export default {
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 添加被测件
|
||||
* 添加测试项
|
||||
* @returns
|
||||
*/
|
||||
save(params = {}) {
|
||||
|
||||
@@ -65,6 +65,7 @@ const useTreeDataStore = defineStore("treeDataStore", {
|
||||
},
|
||||
// 新增删除designDemand后tree显示-注意传的是测试项的key
|
||||
async updateDesignDemandTreeData(data, projrctId) {
|
||||
console.log(data);
|
||||
let temp = data.key.split("-")
|
||||
temp.pop(-1)
|
||||
let roundKey = temp[0]
|
||||
|
||||
@@ -10,10 +10,7 @@
|
||||
<img src="@/assets/img/wxwx-logo.svg" width="45" /><span>{{ $title }}</span>
|
||||
</div>
|
||||
<div class="slogan flex justify-end">
|
||||
<span
|
||||
class="font-extrabold bg-clip-text text-transparent bg-gradient-to-r from-blue-500 to-purple-600"
|
||||
>---- 为测评服务,打造测评高地</span
|
||||
>
|
||||
<span class="font-extrabold bg-clip-text text-transparent bg-gradient-to-r from-blue-500 to-purple-600">---- 为测评服务,打造测评高地</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,14 +25,7 @@
|
||||
{ maxLength: 30, message: '用户名不能超过30个字符' }
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.username"
|
||||
class="w-full"
|
||||
size="large"
|
||||
placeholder="用户名"
|
||||
allow-clear
|
||||
:max-length="30"
|
||||
>
|
||||
<a-input v-model="form.username" class="w-full" size="large" placeholder="用户名" allow-clear :max-length="30">
|
||||
<template #prefix><icon-user /></template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
@@ -48,13 +38,7 @@
|
||||
{ maxLength: 30, message: '密码不超过30字符' }
|
||||
]"
|
||||
>
|
||||
<a-input-password
|
||||
v-model="form.password"
|
||||
placeholder="请输入密码"
|
||||
size="large"
|
||||
allow-clear
|
||||
:max-length="30"
|
||||
>
|
||||
<a-input-password v-model="form.password" placeholder="请输入密码" size="large" allow-clear :max-length="30">
|
||||
<template #prefix><icon-lock /></template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
@@ -70,13 +54,7 @@
|
||||
}
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.code"
|
||||
placeholder="请输入验证码"
|
||||
size="large"
|
||||
allow-clear
|
||||
:max-length="4"
|
||||
>
|
||||
<a-input v-model="form.code" placeholder="请输入验证码" size="large" allow-clear :max-length="4">
|
||||
<template #prefix><icon-safe /></template>
|
||||
<template #append>
|
||||
<verify-code ref="Verify" />
|
||||
@@ -85,9 +63,7 @@
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :hide-label="true" class="mt-5">
|
||||
<a-button html-type="submit" type="primary" long size="large" :loading="loading">
|
||||
登录
|
||||
</a-button>
|
||||
<a-button html-type="submit" type="primary" long size="large" :loading="loading"> 登录 </a-button>
|
||||
</a-form-item>
|
||||
|
||||
<a-divider orientation="center">未来登录方式</a-divider>
|
||||
@@ -115,7 +91,7 @@ const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
// 绑定登录form的数据
|
||||
// const form = reactive({ username: "superAdmin", password: "admin123", code: "" })
|
||||
const form = reactive({ username: "", password: "", code: "" })
|
||||
const form = reactive({ username: "superAdmin", password: "admin123", code: "" })
|
||||
// 获取验证码dom && arco表单loading
|
||||
const Verify = ref(null)
|
||||
const loading = ref(null)
|
||||
@@ -185,7 +161,9 @@ const handleSubmit = async ({ values, errors }) => {
|
||||
top: 50%;
|
||||
margin-top: -255px;
|
||||
border-radius: var(--border-radius-small);
|
||||
box-shadow: rgba(0, 0, 0, 0.25) 0px 14px 28px, rgba(0, 0, 0, 0.22) 0px 10px 10px;
|
||||
box-shadow:
|
||||
rgba(0, 0, 0, 0.25) 0px 14px 28px,
|
||||
rgba(0, 0, 0, 0.22) 0px 10px 10px;
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,12 +49,7 @@ export default defineComponent({
|
||||
{{
|
||||
default: () => (
|
||||
<div class="preview-container">
|
||||
<div
|
||||
class="button-like"
|
||||
ref={buttonLikeRef}
|
||||
onMouseenter={onMouseenter}
|
||||
onMouseleave={onMouseleave}
|
||||
>
|
||||
<div class="button-like" ref={buttonLikeRef} onMouseenter={onMouseenter} onMouseleave={onMouseleave}>
|
||||
<icon-find-replace />
|
||||
{hoverText.value && <span class="ml-2">{hoverText.value}</span>}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="ai-modal-container">
|
||||
<a-modal v-model:visible="visible" width="80%" unmount-on-close draggable>
|
||||
<a-modal v-model:visible="visible" width="80%" unmount-on-close draggable :footer="false">
|
||||
<template #title> AI生成测试项 </template>
|
||||
<div class="flex flex-col">
|
||||
<a-button type="primary" :disabled="generateLoading" @click="generateClick">{{
|
||||
@@ -24,10 +24,10 @@
|
||||
<div class="item-container">
|
||||
<a-input-group>
|
||||
<div class="index-hao">{{ indexTu[index] }}</div>
|
||||
测试项:
|
||||
<span class="label">测试项:</span>
|
||||
<a-input placeholder="测试项标识" v-model="item.ident" :style="{ width: '100px' }" @click.stop.prevent></a-input>
|
||||
<a-input placeholder="测试项名称" v-model="item.title" :style="{ width: '200px' }" @click.stop.prevent></a-input>
|
||||
<a-select placeholder="选择优先级" v-model="item.priority" :style="{ width: '100px' }">
|
||||
<a-input placeholder="测试项名称" v-model="item.title" :style="{ width: '250px' }" @click.stop.prevent></a-input>
|
||||
<a-select placeholder="选择优先级" v-model="item.priority" :style="{ width: '150px' }">
|
||||
<a-option value="1">高</a-option>
|
||||
<a-option value="2">中</a-option>
|
||||
<a-option value="3">低</a-option>
|
||||
@@ -37,7 +37,7 @@
|
||||
{{ type.title }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
<a-select placeholder="选择测试手段" multiple v-model="item.testMethod" :style="{ width: '250px' }">
|
||||
<a-select placeholder="选择测试手段" multiple v-model="item.testMethod" :style="{ width: '400px' }">
|
||||
<a-option v-for="method in testMethod" :key="method.key" :value="method.key">
|
||||
{{ method.title }}
|
||||
</a-option>
|
||||
@@ -56,17 +56,17 @@
|
||||
<tr class="arco-table-tr">
|
||||
<th class="arco-table-th" :width="100">
|
||||
<span class="arco-table-cell arco-table-cell-align-center">
|
||||
<span class="arco-table-th-title">子项序号</span>
|
||||
<span class="arco-table-th-title label">子项序号</span>
|
||||
</span>
|
||||
</th>
|
||||
<th class="arco-table-th" :width="400">
|
||||
<span class="arco-table-cell arco-table-cell-align-center">
|
||||
<span class="arco-table-th-title">测试子项描述</span>
|
||||
<span class="arco-table-th-title label">测试子项描述</span>
|
||||
</span>
|
||||
</th>
|
||||
<th class="arco-table-th" :width="800">
|
||||
<span class="arco-table-cell arco-table-cell-align-center">
|
||||
<span class="arco-table-th-title">测试子项步骤</span>
|
||||
<span class="arco-table-th-title label">测试子项步骤</span>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
@@ -98,7 +98,11 @@
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
<div class="luButton">
|
||||
<a-button :loading="luButtonLoading" type="primary" @click="luButtonClick">确认录入测试项</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<ParentPreview :parent-key="currentKey" />
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
@@ -112,6 +116,9 @@ import OpeAndExpect from "./OpeAndExpect.vue" // 操作和预期子表格
|
||||
import aiApi from "@/api/outs/aiApi"
|
||||
import { Message } from "@arco-design/web-vue"
|
||||
import tool from "@/utils/tool"
|
||||
import { isEmpty } from "lodash-es"
|
||||
import demandApi from "@/api/project/testDemand"
|
||||
import ParentPreview from "@/views/project/ParentPreview/index.vue"
|
||||
|
||||
// 常量
|
||||
const indexTu = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳㉑㉒㉓㉔㉕㉖㉗㉘㉙㉚"
|
||||
@@ -132,6 +139,7 @@ fetchTestType()
|
||||
|
||||
// 初始化设计需求
|
||||
const route = useRoute()
|
||||
const currentKey: string = route.query.key as string
|
||||
const getDesign = async () => {
|
||||
try {
|
||||
const res = await designApi.getDesignDemandOne({ project_id: route.query.id, key: route.query.key })
|
||||
@@ -158,11 +166,30 @@ const generateClick = async () => {
|
||||
// 变量:给AI的问题
|
||||
const question = tool.htmlToTextWithDOM(designObj.value?.description || "")
|
||||
console.log("给AI的问题如下:", question)
|
||||
// 请求后处理结果
|
||||
const res = await aiApi.getAiTestItem({ question: question, stream: false })
|
||||
// 判断真实接口和开发环境接口
|
||||
let tempSolve: any = null
|
||||
if (res.data) {
|
||||
// 说明是开发环境
|
||||
tempSolve = res.data
|
||||
} else {
|
||||
tempSolve = res
|
||||
}
|
||||
const solveRes = JSON.parse(tempSolve.history[0].at(-1))
|
||||
console.log("AI生成测试项结果:", solveRes)
|
||||
// 给Vue渲染测试项
|
||||
dataList.value = solveRes
|
||||
dataList.value.forEach((it: any) => {
|
||||
it.ident = designObj.value.ident
|
||||
it.priority = "1"
|
||||
it.testType = "4"
|
||||
it.testMethod = ["4"]
|
||||
})
|
||||
percent.value = 1.0 // 完成进度
|
||||
console.log("AI接口返回如下", res)
|
||||
Message.success("生成测试项成功,请完善信息后录入数据")
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
percent.value = 0.0
|
||||
} finally {
|
||||
stopProgressSimulation()
|
||||
@@ -199,6 +226,79 @@ onUnmounted(() => {
|
||||
|
||||
// defineModel
|
||||
const visible = defineModel<boolean>("visible", { default: false })
|
||||
|
||||
// 录入按钮相关
|
||||
const luButtonLoading = ref(false)
|
||||
const emit = defineEmits(["updateTable"])
|
||||
const luButtonClick = async () => {
|
||||
// 1.检查是否还未生成测试项
|
||||
if (isEmpty(dataList.value)) {
|
||||
Message.warning("您还未生成测试项,请生成后再试")
|
||||
return
|
||||
}
|
||||
// 2.检查测试项标识、测试项名称、优先级、测试类型、测试手段是否填写
|
||||
const testItem: any = dataList.value.at(0)
|
||||
if (!testItem.title.trim()) {
|
||||
Message.warning("请先填写测试项名称!")
|
||||
return
|
||||
}
|
||||
if (!testItem.demandDescription.trim()) {
|
||||
Message.warning("请填写测试项描述后再试!")
|
||||
return
|
||||
}
|
||||
if (testItem.testMethod.length === 0) {
|
||||
Message.warning("请先选择测试手段后再试!")
|
||||
return
|
||||
}
|
||||
// 3.组装接口需要的数据
|
||||
const projectId = route.query.id
|
||||
const splitKey: string[] = (route.query.key as any).split("-")
|
||||
const round = splitKey[0]
|
||||
const dut = splitKey[1]
|
||||
const designDemand = splitKey.at(-1)
|
||||
const adequacy: string = "测试用例覆盖测试子项要求的全部内容。\n所有用例执行完毕,对于未执行用例说明未执行原因。"
|
||||
const ident = testItem.ident
|
||||
const name = testItem.title
|
||||
const testType = testItem.testType
|
||||
const testMethod = testItem.testMethod
|
||||
const testDesciption = testItem.demandDescription
|
||||
const priority = testItem.priority
|
||||
const testContent = testItem.children.map(({ name: subName, ...rest }) => ({
|
||||
subName,
|
||||
...rest
|
||||
}))
|
||||
// 4.异步录入啦
|
||||
try {
|
||||
// 首先设置状态
|
||||
luButtonLoading.value = true
|
||||
generateLoading.value = true
|
||||
await demandApi.save({
|
||||
projectId,
|
||||
round,
|
||||
dut,
|
||||
designDemand,
|
||||
adequacy,
|
||||
ident,
|
||||
name,
|
||||
testType,
|
||||
testMethod,
|
||||
testDesciption,
|
||||
priority,
|
||||
testContent
|
||||
})
|
||||
// 请求成功后需要:清除dataList内容,关闭弹窗,提示新增成功,刷新树状结构以及表格(给父组件刷新)
|
||||
dataList.value = []
|
||||
visible.value = false
|
||||
Message.success("录入测试项成功")
|
||||
emit("updateTable")
|
||||
} catch (e) {
|
||||
} finally {
|
||||
luButtonLoading.value = false
|
||||
generateLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 悬浮按钮显示上级设计需求内容
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@@ -216,4 +316,12 @@ const visible = defineModel<boolean>("visible", { default: false })
|
||||
:deep(.arco-progress-line) {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
.label {
|
||||
font-weight: 700;
|
||||
}
|
||||
.luButton {
|
||||
margin-left: auto;
|
||||
padding: 10px;
|
||||
padding-right: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
<tr class="arco-table-tr">
|
||||
<th class="arco-table-th" :width="400">
|
||||
<span class="arco-table-cell arco-table-cell-align-center">
|
||||
<span class="arco-table-th-title">操作</span>
|
||||
<span class="arco-table-th-title label">操作</span>
|
||||
</span>
|
||||
</th>
|
||||
<th class="arco-table-th" :width="400">
|
||||
<span class="arco-table-cell arco-table-cell-align-center">
|
||||
<span class="arco-table-th-title">预期</span>
|
||||
<span class="arco-table-th-title label">预期</span>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
<!-- 批量修改优先级 -->
|
||||
<ReplacePriority @modifySuccess="crudRef.refresh()" ref="replacePriorityRef" />
|
||||
<!-- AI-Modal -->
|
||||
<AiModal v-model:visible="ai_modal_visible"></AiModal>
|
||||
<AiModal @updateTable="handleAiRefresh" v-model:visible="ai_modal_visible"></AiModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -81,6 +81,7 @@ import { Message } from "@arco-design/web-vue"
|
||||
import AiButton from "@/components/ai-button/index.vue"
|
||||
import AiModal from "./AiModal.vue"
|
||||
// hooks
|
||||
import { useTreeDataStore } from "@/store"
|
||||
import useCrudOpMore from "./hooks/useCrudOpMore"
|
||||
import useColumn from "./hooks/useColumns"
|
||||
import useRalateDemand from "./hooks/useRalateDemand"
|
||||
@@ -89,6 +90,7 @@ import ReplaceModel from "@/views/project/opeSets/components/DesignTable/Replace
|
||||
import ReplacePriority from "@/views/project/opeSets/components/DemandTable/ReplacePriority.vue"
|
||||
// inits
|
||||
const route = useRoute()
|
||||
const treeDataStore = useTreeDataStore()
|
||||
// refs
|
||||
const crudRef = ref(null)
|
||||
|
||||
@@ -114,8 +116,7 @@ const { projectId, crudOptions, handleBeforeCancel } = useCrudOpMore(crudRef)
|
||||
const crudColumns = useColumn(crudRef)
|
||||
|
||||
// 关联弹窗、关联的事件处理
|
||||
const { visible, relatedData, options, cascaderLoading, computedRelatedData, handleOpenRelationCSX, handleRelatedOk } =
|
||||
useRalateDemand(projectId)
|
||||
const { visible, relatedData, options, cascaderLoading, computedRelatedData, handleOpenRelationCSX, handleRelatedOk } = useRalateDemand(projectId)
|
||||
|
||||
// 标识显示字段
|
||||
const testTypeDict = ref([])
|
||||
@@ -139,10 +140,13 @@ const ai_modal_visible = ref(false)
|
||||
const handleAiButtonClick = () => {
|
||||
ai_modal_visible.value = true
|
||||
}
|
||||
|
||||
const handleAiRefresh = () => {
|
||||
refreshCrudTable()
|
||||
}
|
||||
// 暴露给route-view的刷新表格函数
|
||||
const refreshCrudTable = () => {
|
||||
crudRef.value.refresh()
|
||||
treeDataStore.updateTestDemandTreeData({ key: route.query.key + "-0" }, projectId.value)
|
||||
}
|
||||
defineExpose({ refreshCrudTable })
|
||||
defineOptions({
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
||||
# 后端接口
|
||||
VITE_API_BASE_URL= 'http://localhost:8080'
|
||||
@@ -1,2 +0,0 @@
|
||||
# 后端接口
|
||||
VITE_API_BASE_URL= 'http://localhost:8080'
|
||||
@@ -1,3 +0,0 @@
|
||||
/*.json
|
||||
/*.js
|
||||
dist
|
||||
@@ -1,74 +0,0 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
// Parser that checks the content of the <script> tag
|
||||
parser: '@typescript-eslint/parser',
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
env: {
|
||||
'browser': true,
|
||||
'node': true,
|
||||
'vue/setup-compiler-macros': true,
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: [
|
||||
// Airbnb JavaScript Style Guide https://github.com/airbnb/javascript
|
||||
'airbnb-base',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:import/recommended',
|
||||
'plugin:import/typescript',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project: path.resolve(__dirname, './tsconfig.json'),
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// 用户定义
|
||||
'no-console': 0,
|
||||
'prettier/prettier': 1,
|
||||
// Vue: Recommended rules to be closed or modify
|
||||
'vue/require-default-prop': 0,
|
||||
'vue/singleline-html-element-content-newline': 0,
|
||||
'vue/max-attributes-per-line': 0,
|
||||
// Vue: Add extra rules
|
||||
'vue/custom-event-name-casing': [2, 'camelCase'],
|
||||
'vue/no-v-text': 1,
|
||||
'vue/padding-line-between-blocks': 1,
|
||||
'vue/require-direct-export': 1,
|
||||
'vue/multi-word-component-names': 0,
|
||||
// Allow @ts-ignore comment
|
||||
'@typescript-eslint/ban-ts-comment': 0,
|
||||
'@typescript-eslint/no-unused-vars': 1,
|
||||
'@typescript-eslint/no-empty-function': 1,
|
||||
'@typescript-eslint/no-explicit-any': 0,
|
||||
'import/extensions': [
|
||||
2,
|
||||
'ignorePackages',
|
||||
{
|
||||
js: 'never',
|
||||
jsx: 'never',
|
||||
ts: 'never',
|
||||
tsx: 'never',
|
||||
},
|
||||
],
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
||||
'no-param-reassign': 0,
|
||||
'prefer-regex-literals': 0,
|
||||
'import/no-extraneous-dependencies': 0,
|
||||
'import/prefer-default-export': 0,
|
||||
'@typescript-eslint/no-non-null-assertion': 0,
|
||||
},
|
||||
}
|
||||
10
cdtestplant/.gitignore
vendored
10
cdtestplant/.gitignore
vendored
@@ -1,10 +0,0 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
@@ -1,7 +0,0 @@
|
||||
/dist/*
|
||||
.local
|
||||
.output.js
|
||||
/node_modules/**
|
||||
|
||||
**/*.svg
|
||||
**/*.sh
|
||||
@@ -1,10 +0,0 @@
|
||||
module.exports = {
|
||||
useTabs: false,
|
||||
tabWidth: 4,
|
||||
semi: false,
|
||||
printWidth: 120,
|
||||
singleQuote: true,
|
||||
quoteProps: 'consistent',
|
||||
htmlWhitespaceSensitivity: 'strict',
|
||||
vueIndentScriptAndStyle: true,
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
'stylelint-config-standard',
|
||||
'stylelint-config-rational-order',
|
||||
'stylelint-config-prettier',
|
||||
'stylelint-config-recommended-vue',
|
||||
],
|
||||
defaultSeverity: 'warning',
|
||||
plugins: ['stylelint-order'],
|
||||
rules: {
|
||||
'at-rule-no-unknown': [
|
||||
true,
|
||||
{
|
||||
ignoreAtRules: ['plugin'],
|
||||
},
|
||||
],
|
||||
'rule-empty-line-before': [
|
||||
'always',
|
||||
{
|
||||
except: ['after-single-line-comment', 'first-nested'],
|
||||
},
|
||||
],
|
||||
'selector-pseudo-class-no-unknown': [
|
||||
true,
|
||||
{
|
||||
ignorePseudoClasses: ['deep'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: ['@vue/babel-plugin-jsx'],
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
}
|
||||
15
cdtestplant/components.d.ts
vendored
15
cdtestplant/components.d.ts
vendored
@@ -1,15 +0,0 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
export {}
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* If you use the template method for development, you can use the unplugin-vue-components plugin to enable on-demand loading support.
|
||||
* 按需引入
|
||||
* https://github.com/antfu/unplugin-vue-components
|
||||
* https://arco.design/vue/docs/start
|
||||
* Although the Pro project is full of imported components, this plugin will be used by default.
|
||||
* 虽然Pro项目中是全量引入组件,但此插件会默认使用。
|
||||
*/
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ArcoResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
export default function configArcoResolverPlugin() {
|
||||
const arcoResolverPlugin = Components({
|
||||
dirs: [], // Avoid parsing src/components. 避免解析到src/components
|
||||
deep: false,
|
||||
resolvers: [ArcoResolver()],
|
||||
})
|
||||
return arcoResolverPlugin
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* Theme import
|
||||
* 样式按需引入
|
||||
* https://github.com/arco-design/arco-plugins/blob/main/packages/plugin-vite-vue/README.md
|
||||
* https://arco.design/vue/docs/start
|
||||
*/
|
||||
import { vitePluginForArco } from '@arco-plugins/vite-vue'
|
||||
|
||||
export default function configArcoStyleImportPlugin() {
|
||||
const arcoResolverPlugin = vitePluginForArco({})
|
||||
return arcoResolverPlugin
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated
|
||||
* gzip压缩
|
||||
* https://github.com/anncwb/vite-plugin-compression
|
||||
*/
|
||||
import type { Plugin } from 'vite'
|
||||
import compressPlugin from 'vite-plugin-compression'
|
||||
|
||||
export default function configCompressPlugin(compress: 'gzip' | 'brotli', deleteOriginFile = false): Plugin | Plugin[] {
|
||||
const plugins: Plugin[] = []
|
||||
|
||||
if (compress === 'gzip') {
|
||||
plugins.push(
|
||||
compressPlugin({
|
||||
ext: '.gz',
|
||||
deleteOriginFile,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (compress === 'brotli') {
|
||||
plugins.push(
|
||||
compressPlugin({
|
||||
ext: '.br',
|
||||
algorithm: 'brotliCompress',
|
||||
deleteOriginFile,
|
||||
})
|
||||
)
|
||||
}
|
||||
return plugins
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* Image resource files used to compress the output of the production environment
|
||||
* 图片压缩
|
||||
* https://github.com/anncwb/vite-plugin-imagemin
|
||||
*/
|
||||
import viteImagemin from 'vite-plugin-imagemin'
|
||||
|
||||
export default function configImageminPlugin() {
|
||||
const imageminPlugin = viteImagemin({
|
||||
gifsicle: {
|
||||
optimizationLevel: 7,
|
||||
interlaced: false,
|
||||
},
|
||||
optipng: {
|
||||
optimizationLevel: 7,
|
||||
},
|
||||
mozjpeg: {
|
||||
quality: 20,
|
||||
},
|
||||
pngquant: {
|
||||
quality: [0.8, 0.9],
|
||||
speed: 4,
|
||||
},
|
||||
svgo: {
|
||||
plugins: [
|
||||
{
|
||||
name: 'removeViewBox',
|
||||
},
|
||||
{
|
||||
name: 'removeEmptyAttrs',
|
||||
active: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
return imageminPlugin
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Generation packaging analysis
|
||||
* 生成打包分析
|
||||
*/
|
||||
import visualizer from 'rollup-plugin-visualizer'
|
||||
import { isReportMode } from '../utils'
|
||||
|
||||
export default function configVisualizerPlugin() {
|
||||
if (isReportMode()) {
|
||||
return visualizer({
|
||||
filename: './node_modules/.cache/visualizer/stats.html',
|
||||
open: true,
|
||||
gzipSize: true,
|
||||
brotliSize: true,
|
||||
})
|
||||
}
|
||||
return []
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Whether to generate package preview
|
||||
* 是否生成打包报告
|
||||
*/
|
||||
export default {}
|
||||
|
||||
export function isReportMode(): boolean {
|
||||
return process.env.REPORT === 'true'
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { resolve } from 'path'
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import svgLoader from 'vite-svg-loader'
|
||||
import configArcoStyleImportPlugin from './plugin/arcoStyleImport'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue(), vueJsx(), svgLoader({ svgoConfig: {} }), configArcoStyleImportPlugin()],
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
find: '@',
|
||||
replacement: resolve(__dirname, '../src'),
|
||||
},
|
||||
{
|
||||
find: 'assets',
|
||||
replacement: resolve(__dirname, '../src/assets'),
|
||||
},
|
||||
{
|
||||
find: 'vue-i18n',
|
||||
replacement: 'vue-i18n/dist/vue-i18n.cjs.js', // Resolve the i18n warning issue
|
||||
},
|
||||
{
|
||||
find: 'vue',
|
||||
replacement: 'vue/dist/vue.esm-bundler.js', // compile template
|
||||
},
|
||||
],
|
||||
extensions: ['.ts', '.js'],
|
||||
},
|
||||
define: {
|
||||
'process.env': {},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
less: {
|
||||
modifyVars: {
|
||||
hack: `true; @import (reference) "${resolve('src/assets/style/breakpoint.less')}";`,
|
||||
},
|
||||
javascriptEnabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,23 +0,0 @@
|
||||
import { mergeConfig } from 'vite'
|
||||
import eslint from 'vite-plugin-eslint'
|
||||
import baseConfig from './vite.config.base'
|
||||
|
||||
export default mergeConfig(
|
||||
{
|
||||
mode: 'development',
|
||||
server: {
|
||||
open: true,
|
||||
fs: {
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
eslint({
|
||||
cache: false,
|
||||
include: ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.vue'],
|
||||
exclude: ['node_modules'],
|
||||
}),
|
||||
],
|
||||
},
|
||||
baseConfig
|
||||
)
|
||||
@@ -1,31 +0,0 @@
|
||||
import { mergeConfig } from 'vite'
|
||||
import baseConfig from './vite.config.base'
|
||||
import configCompressPlugin from './plugin/compress'
|
||||
import configVisualizerPlugin from './plugin/visualizer'
|
||||
import configArcoResolverPlugin from './plugin/arcoResolver'
|
||||
import configImageminPlugin from './plugin/imagemin'
|
||||
|
||||
export default mergeConfig(
|
||||
{
|
||||
mode: 'production',
|
||||
plugins: [
|
||||
configCompressPlugin('gzip'),
|
||||
configVisualizerPlugin(),
|
||||
configArcoResolverPlugin(),
|
||||
configImageminPlugin(),
|
||||
],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
arco: ['@arco-design/web-vue'],
|
||||
chart: ['echarts', 'vue-echarts'],
|
||||
vue: ['vue', 'vue-router', 'pinia', '@vueuse/core', 'vue-i18n'],
|
||||
},
|
||||
},
|
||||
},
|
||||
chunkSizeWarningLimit: 2000,
|
||||
},
|
||||
},
|
||||
baseConfig
|
||||
)
|
||||
@@ -1,18 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
href="https://unpkg.byted-static.com/latest/byted/arco-config/assets/favicon.ico"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>成都测试管理平台</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
18468
cdtestplant/package-lock.json
generated
18468
cdtestplant/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,104 +0,0 @@
|
||||
{
|
||||
"name": "cdtest-plant",
|
||||
"description": "a arcovue test plant",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"author": "ArcoDesign Team",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite --config ./config/vite.config.dev.ts",
|
||||
"build": "vue-tsc --noEmit && vite build --config ./config/vite.config.prod.ts",
|
||||
"report": "cross-env REPORT=true npm run build",
|
||||
"preview": "npm run build && vite preview --host",
|
||||
"type:check": "vue-tsc --noEmit --skipLibCheck",
|
||||
"lint-staged": "npx lint-staged",
|
||||
"prepare": "husky install",
|
||||
"prettier": "prettier --write ."
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts,jsx,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --fix"
|
||||
],
|
||||
"*.vue": [
|
||||
"stylelint --fix",
|
||||
"prettier --write",
|
||||
"eslint --fix"
|
||||
],
|
||||
"*.{less,css}": [
|
||||
"stylelint --fix",
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@arco-design/web-vue": "^2.44.7",
|
||||
"@vueuse/core": "^9.3.0",
|
||||
"arco-design-pro-vue": "^2.7.2",
|
||||
"axios": "^0.24.0",
|
||||
"dayjs": "^1.11.5",
|
||||
"echarts": "^5.4.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mitt": "^3.0.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.0.23",
|
||||
"query-string": "^8.0.3",
|
||||
"sortablejs": "^1.15.0",
|
||||
"vue": "^3.2.40",
|
||||
"vue-echarts": "^6.2.3",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arco-plugins/vite-vue": "^1.4.5",
|
||||
"@commitlint/cli": "^17.1.2",
|
||||
"@commitlint/config-conventional": "^17.1.0",
|
||||
"@types/lodash": "^4.14.186",
|
||||
"@types/mockjs": "^1.0.7",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/sortablejs": "^1.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
||||
"@typescript-eslint/parser": "^5.40.0",
|
||||
"@vitejs/plugin-vue": "^3.1.2",
|
||||
"@vitejs/plugin-vue-jsx": "^2.0.1",
|
||||
"@vue/babel-plugin-jsx": "^1.1.1",
|
||||
"consola": "^2.15.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.25.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vue": "^9.6.0",
|
||||
"husky": "^8.0.1",
|
||||
"less": "^4.1.3",
|
||||
"lint-staged": "^13.0.3",
|
||||
"mockjs": "^1.1.0",
|
||||
"postcss-html": "^1.5.0",
|
||||
"prettier": "^2.7.1",
|
||||
"rollup": "^3.9.1",
|
||||
"rollup-plugin-visualizer": "^5.8.2",
|
||||
"stylelint": "^14.13.0",
|
||||
"stylelint-config-prettier": "^9.0.3",
|
||||
"stylelint-config-rational-order": "^0.1.2",
|
||||
"stylelint-config-recommended-vue": "^1.4.0",
|
||||
"stylelint-config-standard": "^29.0.0",
|
||||
"stylelint-order": "^5.0.0",
|
||||
"typescript": "^4.8.4",
|
||||
"unplugin-vue-components": "^0.24.1",
|
||||
"vite": "^3.2.5",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-imagemin": "^0.6.1",
|
||||
"vite-svg-loader": "^3.6.0",
|
||||
"vue-tsc": "^1.0.14"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"bin-wrapper": "npm:bin-wrapper-china",
|
||||
"rollup": "^2.56.3",
|
||||
"gifsicle": "5.2.0"
|
||||
}
|
||||
}
|
||||
14858
cdtestplant/pnpm-lock.yaml
generated
14858
cdtestplant/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<a-config-provider :locale="locale">
|
||||
<router-view />
|
||||
<global-setting />
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import enUS from '@arco-design/web-vue/es/locale/lang/en-us'
|
||||
import zhCN from '@arco-design/web-vue/es/locale/lang/zh-cn'
|
||||
import GlobalSetting from '@/components/global-setting/index.vue'
|
||||
import useLocale from '@/hooks/locale'
|
||||
|
||||
const { currentLocale } = useLocale()
|
||||
const locale = computed(() => {
|
||||
switch (currentLocale.value) {
|
||||
case 'zh-CN':
|
||||
return zhCN
|
||||
case 'en-US':
|
||||
return enUS
|
||||
default:
|
||||
return enUS
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,22 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import type { TableData } from '@arco-design/web-vue/es/table/interface'
|
||||
|
||||
export interface ContentDataRecord {
|
||||
x: string
|
||||
y: number
|
||||
}
|
||||
|
||||
export function queryContentData() {
|
||||
return axios.get<ContentDataRecord[]>('/api/content-data')
|
||||
}
|
||||
|
||||
export interface PopularRecord {
|
||||
key: number
|
||||
clickNumber: string
|
||||
title: string
|
||||
increases: number
|
||||
}
|
||||
|
||||
export function queryPopularList(params: { type: string }) {
|
||||
return axios.get<TableData[]>('/api/popular/list', { params })
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { useUserStore } from '@/store'
|
||||
import { getToken } from '@/utils/auth'
|
||||
|
||||
export interface HttpResponse<T = unknown> {
|
||||
status: number
|
||||
msg: string
|
||||
code: number
|
||||
data: T
|
||||
}
|
||||
|
||||
if (import.meta.env.VITE_API_BASE_URL) {
|
||||
axios.defaults.baseURL = import.meta.env.VITE_API_BASE_URL
|
||||
}
|
||||
|
||||
// 添加请求拦截器
|
||||
axios.interceptors.request.use(
|
||||
(config: AxiosRequestConfig) => {
|
||||
// 让每个请求携带令牌
|
||||
// 使用JWT token
|
||||
// 授权是一个自定义header密钥
|
||||
// 获取localStorage中token,如果有token,头没有则加上
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
if (!config.headers) {
|
||||
config.headers = {}
|
||||
}
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
// 做点什么
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
// 添加响应拦截器
|
||||
axios.interceptors.response.use(
|
||||
(response: AxiosResponse<HttpResponse>) => {
|
||||
const res = response.data
|
||||
// 如果自定义代码不是20000,则判断为错误
|
||||
if (res.code !== 20000) {
|
||||
Message.error({
|
||||
content: res.msg || 'Error',
|
||||
duration: 5 * 1000,
|
||||
})
|
||||
// 50008: 非法令牌;50012:其他客户端登录;50014:令牌过期
|
||||
if ([50008, 50012, 50014].includes(res.code) && response.config.url !== '/api/user/info') {
|
||||
Modal.error({
|
||||
title: 'Confirm logout',
|
||||
content: '您已注销,您可以取消以留在此页面,也可以重新登录',
|
||||
okText: 'Re-Login',
|
||||
async onOk() {
|
||||
const userStore = useUserStore()
|
||||
|
||||
await userStore.logout()
|
||||
window.location.reload()
|
||||
},
|
||||
})
|
||||
}
|
||||
return Promise.reject(new Error(res.msg || 'Error'))
|
||||
}
|
||||
return res
|
||||
},
|
||||
(error) => {
|
||||
Message.error({
|
||||
content: error.msg || '请求错误',
|
||||
duration: 5 * 1000,
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
@@ -1,38 +0,0 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export interface MessageRecord {
|
||||
id: number
|
||||
type: string
|
||||
title: string
|
||||
subTitle: string
|
||||
avatar?: string
|
||||
content: string
|
||||
time: string
|
||||
status: 0 | 1
|
||||
messageType?: number
|
||||
}
|
||||
export type MessageListType = MessageRecord[]
|
||||
|
||||
export function queryMessageList() {
|
||||
return axios.post<MessageListType>('/api/message/list')
|
||||
}
|
||||
|
||||
interface MessageStatus {
|
||||
ids: number[]
|
||||
}
|
||||
|
||||
export function setMessageStatus(data: MessageStatus) {
|
||||
return axios.post<MessageListType>('/api/message/read', data)
|
||||
}
|
||||
|
||||
export interface ChatRecord {
|
||||
id: number
|
||||
username: string
|
||||
content: string
|
||||
time: string
|
||||
isCollect: boolean
|
||||
}
|
||||
|
||||
export function queryChatList() {
|
||||
return axios.post<ChatRecord[]>('/api/chat/list')
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import type { RouteRecordNormalized } from 'vue-router'
|
||||
import { UserState } from '@/store/modules/user/types'
|
||||
|
||||
export interface LoginData {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface LoginRes {
|
||||
token: string
|
||||
}
|
||||
export function login(data: LoginData) {
|
||||
return axios.post<LoginRes>('/api/user/login', data)
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return axios.post<LoginRes>('/api/user/logout')
|
||||
}
|
||||
|
||||
export function getUserInfo() {
|
||||
return axios.post<UserState>('/api/user/info')
|
||||
}
|
||||
|
||||
export function getMenuList() {
|
||||
return axios.post<RouteRecordNormalized[]>('/api/user/menu')
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import qs from 'query-string'
|
||||
|
||||
export interface PolicyParams {
|
||||
current: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
// 请求用户列表接口
|
||||
export function getUserList(params: PolicyParams) {
|
||||
return axios.get<any>('/api/user/list', {
|
||||
params,
|
||||
paramsSerializer: (obj) => {
|
||||
return qs.stringify(obj)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 请求用户列表删除接口
|
||||
export function deleteUserList(data: any) {
|
||||
return axios.post<any>('/api/user/delete', data)
|
||||
}
|
||||
|
||||
// 全条件搜索接口
|
||||
export function getUserListAll(data: any) {
|
||||
return axios.post<any>('/api/user/all', data)
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 62 KiB |
@@ -1,12 +0,0 @@
|
||||
<svg width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.37754 16.9795L12.7498 9.43027C14.7163 7.41663 17.9428 7.37837 19.9564 9.34482C19.9852 9.37297 20.0137 9.40145 20.0418 9.43027L20.1221 9.51243C22.1049 11.5429 22.1049 14.7847 20.1221 16.8152L12.7498 24.3644C10.7834 26.378 7.55686 26.4163 5.54322 24.4498C5.5144 24.4217 5.48592 24.3932 5.45777 24.3644L5.37754 24.2822C3.39468 22.2518 3.39468 19.0099 5.37754 16.9795Z" fill="#12D2AC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0479 9.43034L27.3399 16.8974C29.3674 18.9735 29.3674 22.2883 27.3399 24.3644C25.3735 26.3781 22.147 26.4163 20.1333 24.4499C20.1045 24.4217 20.076 24.3933 20.0479 24.3644L12.7558 16.8974C10.7284 14.8213 10.7284 11.5065 12.7558 9.43034C14.7223 7.4167 17.9488 7.37844 19.9624 9.34489C19.9912 9.37304 20.0197 9.40152 20.0479 9.43034Z" fill="#307AF2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.1321 9.52163L23.6851 13.1599L16.3931 20.627L9.10103 13.1599L12.6541 9.52163C14.6707 7.45664 17.9794 7.4174 20.0444 9.434C20.074 9.46286 20.1032 9.49207 20.1321 9.52163Z" fill="#0057FE"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="26" height="19" fill="white" transform="translate(3.5 7)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,19 +0,0 @@
|
||||
// ==============breakpoint============
|
||||
|
||||
// Extra small screen / phone
|
||||
@screen-xs: 480px;
|
||||
|
||||
// Small screen / tablet
|
||||
@screen-sm: 576px;
|
||||
|
||||
// Medium screen / desktop
|
||||
@screen-md: 768px;
|
||||
|
||||
// Large screen / wide desktop
|
||||
@screen-lg: 992px;
|
||||
|
||||
// Extra large screen / full hd
|
||||
@screen-xl: 1200px;
|
||||
|
||||
// Extra extra large screen / large desktop
|
||||
@screen-xxl: 1600px;
|
||||
@@ -1,90 +0,0 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
background-color: var(--color-bg-1);
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.echarts-tooltip-diy {
|
||||
background: linear-gradient(304.17deg, rgba(253, 254, 255, 0.6) -6.04%, rgba(244, 247, 252, 0.6) 85.2%) !important;
|
||||
border: none !important;
|
||||
backdrop-filter: blur(10px) !important;
|
||||
/* Note: backdrop-filter has minimal browser support */
|
||||
|
||||
border-radius: 6px !important;
|
||||
.content-panel {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 9px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
width: 164px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
box-shadow: 6px 0px 20px rgba(34, 87, 188, 0.1);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.tooltip-title {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
.tooltip-title,
|
||||
.tooltip-value {
|
||||
font-size: 13px;
|
||||
line-height: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: right;
|
||||
color: #1d2129;
|
||||
font-weight: bold;
|
||||
}
|
||||
.tooltip-item-icon {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.general-card {
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
& > .arco-card-header {
|
||||
height: auto;
|
||||
padding: 20px;
|
||||
border: none;
|
||||
}
|
||||
& > .arco-card-body {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.split-line {
|
||||
border-color: rgb(var(--gray-2));
|
||||
}
|
||||
|
||||
.arco-table-cell {
|
||||
.circle {
|
||||
display: inline-block;
|
||||
margin-right: 4px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: rgb(var(--blue-6));
|
||||
&.pass {
|
||||
background-color: rgb(var(--green-6));
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
||||
import chenForm from '@/base-ui/from/src/form.vue'
|
||||
|
||||
export default chenForm
|
||||
@@ -1,95 +0,0 @@
|
||||
<template>
|
||||
<div class="chen-form">
|
||||
<div class="header">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<a-form :model="modelValue">
|
||||
<a-row>
|
||||
<template v-for="item in (formItems as any[])" :key="item.label">
|
||||
<a-col v-bind="colLayout">
|
||||
<a-form-item
|
||||
v-if="!item.isHidden"
|
||||
:field="item.field"
|
||||
:rules="item.rules"
|
||||
:validate-trigger="item.trigger"
|
||||
:label="item.label"
|
||||
:placeholder="item.placeholder"
|
||||
>
|
||||
<template v-if="item.type === 'input'">
|
||||
<a-input
|
||||
allow-clear
|
||||
:placeholder="item.placeholder"
|
||||
:model-value="modelValue[`${item.field}`]"
|
||||
@update:model-value="handleValueChange($event, item.field)"
|
||||
></a-input>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'password'">
|
||||
<a-input-password
|
||||
allow-clear
|
||||
:placeholder="item.placeholder"
|
||||
:model-value="modelValue[`${item.field}`]"
|
||||
@update:model-value="handleValueChange($event, item.field)"
|
||||
></a-input-password>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'select'">
|
||||
<a-select
|
||||
allow-clear
|
||||
:placeholder="item.placeholder"
|
||||
style="width: 100%"
|
||||
:model-value="modelValue[`${item.field}`]"
|
||||
@update:model-value="handleValueChange($event, item.field)"
|
||||
>
|
||||
<a-option
|
||||
v-for="option in (item as any).options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
:label="option.label"
|
||||
></a-option>
|
||||
</a-select>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'datepicker'">
|
||||
<a-date-picker
|
||||
style="width: 100%"
|
||||
:model-value="modelValue[`${item.field}`]"
|
||||
@update:model-value="handleValueChange($event, item.field)"
|
||||
></a-date-picker>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</template>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, defineEmits } from 'vue'
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
formItems: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
colLayout: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
xs: 24,
|
||||
sm: 24,
|
||||
md: 24,
|
||||
lg: 24,
|
||||
xl: 24,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const handleValueChange = (value: any, field: string) => {
|
||||
emit('update:modelValue', { ...props.modelValue, [field]: value })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,35 +0,0 @@
|
||||
<template>
|
||||
<a-breadcrumb class="container-breadcrumb">
|
||||
<a-breadcrumb-item>
|
||||
<icon-apps />
|
||||
</a-breadcrumb-item>
|
||||
<a-breadcrumb-item v-for="item in items" :key="item">
|
||||
{{ $t(item) }}
|
||||
</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue'
|
||||
|
||||
defineProps({
|
||||
items: {
|
||||
type: Array as PropType<string[]>,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container-breadcrumb {
|
||||
margin: 16px 0;
|
||||
:deep(.arco-breadcrumb-item) {
|
||||
color: rgb(var(--gray-6));
|
||||
&:last-child {
|
||||
color: rgb(var(--gray-8));
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,42 +0,0 @@
|
||||
<template>
|
||||
<VCharts v-if="renderChart" :option="options" :autoresize="autoResize" :style="{ width, height }" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, nextTick } from 'vue'
|
||||
import VCharts from 'vue-echarts'
|
||||
// import { useAppStore } from '@/store';
|
||||
|
||||
defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
autoResize: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
})
|
||||
// const appStore = useAppStore();
|
||||
// const theme = computed(() => {
|
||||
// if (appStore.theme === 'dark') return 'dark';
|
||||
// return '';
|
||||
// });
|
||||
const renderChart = ref(false)
|
||||
// wait container expand
|
||||
nextTick(() => {
|
||||
renderChart.value = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
@@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<a-layout-footer class="footer">成都测试管理平台</a-layout-footer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 40px;
|
||||
color: var(--color-text-2);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,73 +0,0 @@
|
||||
<template>
|
||||
<div class="block">
|
||||
<h5 class="title">{{ title }}</h5>
|
||||
<div v-for="option in options" :key="option.name" class="switch-wrapper">
|
||||
<span>{{ $t(option.name) }}</span>
|
||||
<form-wrapper
|
||||
:type="option.type || 'switch'"
|
||||
:name="option.key"
|
||||
:default-value="option.defaultVal"
|
||||
@input-change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue'
|
||||
import { useAppStore } from '@/store'
|
||||
import FormWrapper from './form-wrapper.vue'
|
||||
|
||||
interface OptionsProps {
|
||||
name: string
|
||||
key: string
|
||||
type?: string
|
||||
defaultVal?: boolean | string | number
|
||||
}
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
options: {
|
||||
type: Array as PropType<OptionsProps[]>,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
})
|
||||
const appStore = useAppStore()
|
||||
const handleChange = async ({ key, value }: { key: string; value: unknown }) => {
|
||||
if (key === 'colorWeak') {
|
||||
document.body.style.filter = value ? 'invert(80%)' : 'none'
|
||||
}
|
||||
if (key === 'menuFromServer' && value) {
|
||||
await appStore.fetchServerMenuConfig()
|
||||
}
|
||||
if (key === 'topMenu') {
|
||||
appStore.updateSettings({
|
||||
menuCollapse: false,
|
||||
})
|
||||
}
|
||||
appStore.updateSettings({ [key]: value })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.block {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 10px 0;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.switch-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 32px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,34 +0,0 @@
|
||||
<template>
|
||||
<a-input-number
|
||||
v-if="type === 'number'"
|
||||
:style="{ width: '80px' }"
|
||||
size="small"
|
||||
:default-value="(defaultValue as number)"
|
||||
@change="handleChange"
|
||||
/>
|
||||
<a-switch v-else :default-checked="(defaultValue as boolean)" size="small" @change="handleChange" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
defaultValue: {
|
||||
type: [String, Boolean, Number],
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['inputChange'])
|
||||
const handleChange = (value: unknown) => {
|
||||
emit('inputChange', {
|
||||
value,
|
||||
key: props.name,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -1,98 +0,0 @@
|
||||
<template>
|
||||
<div v-if="!appStore.navbar" class="fixed-settings" @click="setVisible">
|
||||
<a-button type="primary">
|
||||
<template #icon>
|
||||
<icon-settings />
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
<a-drawer
|
||||
:width="300"
|
||||
unmount-on-close
|
||||
:visible="visible"
|
||||
:cancel-text="$t('settings.close')"
|
||||
:ok-text="$t('settings.copySettings')"
|
||||
@ok="copySettings"
|
||||
@cancel="cancel"
|
||||
>
|
||||
<template #title> {{ $t('settings.title') }} </template>
|
||||
<Block :options="contentOpts" :title="$t('settings.content')" />
|
||||
<Block :options="othersOpts" :title="$t('settings.otherSettings')" />
|
||||
<a-alert>{{ $t('settings.alertContent') }}</a-alert>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { useAppStore } from '@/store'
|
||||
import Block from './block.vue'
|
||||
|
||||
const emit = defineEmits(['cancel'])
|
||||
|
||||
const appStore = useAppStore()
|
||||
const { t } = useI18n()
|
||||
const { copy } = useClipboard()
|
||||
const visible = computed(() => appStore.globalSettings)
|
||||
const contentOpts = computed(() => [
|
||||
{ name: 'settings.navbar', key: 'navbar', defaultVal: appStore.navbar },
|
||||
{
|
||||
name: 'settings.menu',
|
||||
key: 'menu',
|
||||
defaultVal: appStore.menu,
|
||||
},
|
||||
{
|
||||
name: 'settings.topMenu',
|
||||
key: 'topMenu',
|
||||
defaultVal: appStore.topMenu,
|
||||
},
|
||||
{ name: 'settings.footer', key: 'footer', defaultVal: appStore.footer },
|
||||
{ name: 'settings.tabBar', key: 'tabBar', defaultVal: appStore.tabBar },
|
||||
{
|
||||
name: 'settings.menuFromServer',
|
||||
key: 'menuFromServer',
|
||||
defaultVal: appStore.menuFromServer,
|
||||
},
|
||||
{
|
||||
name: 'settings.menuWidth',
|
||||
key: 'menuWidth',
|
||||
defaultVal: appStore.menuWidth,
|
||||
type: 'number',
|
||||
},
|
||||
])
|
||||
const othersOpts = computed(() => [
|
||||
{
|
||||
name: 'settings.colorWeak',
|
||||
key: 'colorWeak',
|
||||
defaultVal: appStore.colorWeak,
|
||||
},
|
||||
])
|
||||
|
||||
const cancel = () => {
|
||||
appStore.updateSettings({ globalSettings: false })
|
||||
emit('cancel')
|
||||
}
|
||||
const copySettings = async () => {
|
||||
const text = JSON.stringify(appStore.$state, null, 2)
|
||||
await copy(text)
|
||||
Message.success(t('settings.copySettings.message'))
|
||||
}
|
||||
const setVisible = () => {
|
||||
appStore.updateSettings({ globalSettings: true })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.fixed-settings {
|
||||
position: fixed;
|
||||
top: 280px;
|
||||
right: 0;
|
||||
|
||||
svg {
|
||||
font-size: 18px;
|
||||
vertical-align: -4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,36 +0,0 @@
|
||||
// 全局注册内容
|
||||
import { App } from 'vue'
|
||||
import { use } from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts'
|
||||
import {
|
||||
GridComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
DataZoomComponent,
|
||||
GraphicComponent,
|
||||
} from 'echarts/components'
|
||||
import Chart from './chart/index.vue'
|
||||
import Breadcrumb from './breadcrumb/index.vue'
|
||||
|
||||
// 手动引入ECharts模块以减小包装尺寸
|
||||
|
||||
use([
|
||||
CanvasRenderer,
|
||||
BarChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
RadarChart,
|
||||
GridComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
DataZoomComponent,
|
||||
GraphicComponent,
|
||||
])
|
||||
|
||||
export default {
|
||||
install(Vue: App) {
|
||||
Vue.component('Chart', Chart)
|
||||
Vue.component('Breadcrumb', Breadcrumb)
|
||||
},
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
<script lang="tsx">
|
||||
import { defineComponent, ref, h, compile, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter, RouteRecordRaw } from 'vue-router'
|
||||
import type { RouteMeta } from 'vue-router'
|
||||
import { useAppStore } from '@/store'
|
||||
import { listenerRouteChange } from '@/utils/route-listener'
|
||||
import { openWindow, regexUrl } from '@/utils'
|
||||
import useMenuTree from './use-menu-tree'
|
||||
|
||||
export default defineComponent({
|
||||
emit: ['collapse'],
|
||||
setup() {
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { menuTree } = useMenuTree()
|
||||
const collapsed = computed({
|
||||
get() {
|
||||
if (appStore.device === 'desktop') return appStore.menuCollapse
|
||||
return false
|
||||
},
|
||||
set(value: boolean) {
|
||||
appStore.updateSettings({ menuCollapse: value })
|
||||
},
|
||||
})
|
||||
|
||||
const topMenu = computed(() => appStore.topMenu)
|
||||
const openKeys = ref<string[]>([])
|
||||
const selectedKey = ref<string[]>([])
|
||||
|
||||
const goto = (item: RouteRecordRaw) => {
|
||||
// Open external link
|
||||
if (regexUrl.test(item.path)) {
|
||||
openWindow(item.path)
|
||||
selectedKey.value = [item.name as string]
|
||||
return
|
||||
}
|
||||
// Eliminate external link side effects
|
||||
const { hideInMenu, activeMenu } = item.meta as RouteMeta
|
||||
if (route.name === item.name && !hideInMenu && !activeMenu) {
|
||||
selectedKey.value = [item.name as string]
|
||||
return
|
||||
}
|
||||
// Trigger router change
|
||||
router.push({
|
||||
name: item.name,
|
||||
})
|
||||
}
|
||||
const findMenuOpenKeys = (target: string) => {
|
||||
const result: string[] = []
|
||||
let isFind = false
|
||||
const backtrack = (item: RouteRecordRaw, keys: string[]) => {
|
||||
if (item.name === target) {
|
||||
isFind = true
|
||||
result.push(...keys)
|
||||
return
|
||||
}
|
||||
if (item.children?.length) {
|
||||
item.children.forEach((el) => {
|
||||
backtrack(el, [...keys, el.name as string])
|
||||
})
|
||||
}
|
||||
}
|
||||
menuTree.value.forEach((el: RouteRecordRaw) => {
|
||||
if (isFind) return // Performance optimization
|
||||
backtrack(el, [el.name as string])
|
||||
})
|
||||
return result
|
||||
}
|
||||
listenerRouteChange((newRoute) => {
|
||||
const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta
|
||||
if (requiresAuth && (!hideInMenu || activeMenu)) {
|
||||
const menuOpenKeys = findMenuOpenKeys((activeMenu || newRoute.name) as string)
|
||||
|
||||
const keySet = new Set([...menuOpenKeys, ...openKeys.value])
|
||||
openKeys.value = [...keySet]
|
||||
|
||||
selectedKey.value = [activeMenu || menuOpenKeys[menuOpenKeys.length - 1]]
|
||||
}
|
||||
}, true)
|
||||
const setCollapse = (val: boolean) => {
|
||||
if (appStore.device === 'desktop') appStore.updateSettings({ menuCollapse: val })
|
||||
}
|
||||
|
||||
const renderSubMenu = () => {
|
||||
function travel(_route: RouteRecordRaw[], nodes = []) {
|
||||
if (_route) {
|
||||
_route.forEach((element) => {
|
||||
// This is demo, modify nodes as needed
|
||||
const icon = element?.meta?.icon ? () => h(compile(`<${element?.meta?.icon}/>`)) : null
|
||||
const node =
|
||||
element?.children && element?.children.length !== 0 ? (
|
||||
<a-sub-menu
|
||||
key={element?.name}
|
||||
v-slots={{
|
||||
icon,
|
||||
title: () => h(compile(t(element?.meta?.locale || ''))),
|
||||
}}
|
||||
>
|
||||
{travel(element?.children)}
|
||||
</a-sub-menu>
|
||||
) : (
|
||||
<a-menu-item key={element?.name} v-slots={{ icon }} onClick={() => goto(element)}>
|
||||
{t(element?.meta?.locale || '')}
|
||||
</a-menu-item>
|
||||
)
|
||||
nodes.push(node as never)
|
||||
})
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
return travel(menuTree.value)
|
||||
}
|
||||
|
||||
return () => (
|
||||
<a-menu
|
||||
mode={topMenu.value ? 'horizontal' : 'vertical'}
|
||||
v-model:collapsed={collapsed.value}
|
||||
v-model:open-keys={openKeys.value}
|
||||
show-collapse-button={appStore.device !== 'mobile'}
|
||||
auto-open={false}
|
||||
selected-keys={selectedKey.value}
|
||||
auto-open-selected={true}
|
||||
level-indent={34}
|
||||
style="height: 100%;width:100%;"
|
||||
onCollapse={setCollapse}
|
||||
>
|
||||
{renderSubMenu()}
|
||||
</a-menu>
|
||||
)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.arco-menu-inner) {
|
||||
.arco-menu-inline-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.arco-icon {
|
||||
&:not(.arco-icon-down) {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,67 +0,0 @@
|
||||
import { computed } from 'vue'
|
||||
import { RouteRecordRaw, RouteRecordNormalized } from 'vue-router'
|
||||
import usePermission from '@/hooks/permission'
|
||||
import { useAppStore } from '@/store'
|
||||
import appClientMenus from '@/router/app-menus'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
export default function useMenuTree() {
|
||||
const permission = usePermission()
|
||||
const appStore = useAppStore()
|
||||
const appRoute = computed(() => {
|
||||
if (appStore.menuFromServer) {
|
||||
return appStore.appAsyncMenus
|
||||
}
|
||||
return appClientMenus
|
||||
})
|
||||
const menuTree = computed(() => {
|
||||
const copyRouter = cloneDeep(appRoute.value) as RouteRecordNormalized[]
|
||||
copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => {
|
||||
return (a.meta.order || 0) - (b.meta.order || 0)
|
||||
})
|
||||
function travel(_routes: RouteRecordRaw[], layer: number) {
|
||||
if (!_routes) return null
|
||||
|
||||
const collector: any = _routes.map((element) => {
|
||||
// no access
|
||||
if (!permission.accessRouter(element)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// leaf node
|
||||
if (element.meta?.hideChildrenInMenu || !element.children) {
|
||||
element.children = []
|
||||
return element
|
||||
}
|
||||
|
||||
// route filter hideInMenu true
|
||||
element.children = element.children.filter((x) => x.meta?.hideInMenu !== true)
|
||||
|
||||
// Associated child node
|
||||
const subItem = travel(element.children, layer + 1)
|
||||
|
||||
if (subItem.length) {
|
||||
element.children = subItem
|
||||
return element
|
||||
}
|
||||
// the else logic
|
||||
if (layer > 1) {
|
||||
element.children = subItem
|
||||
return element
|
||||
}
|
||||
|
||||
if (element.meta?.hideInMenu === false) {
|
||||
return element
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
return collector.filter(Boolean)
|
||||
}
|
||||
return travel(copyRouter, 0)
|
||||
})
|
||||
|
||||
return {
|
||||
menuTree,
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
<template>
|
||||
<a-spin style="display: block" :loading="loading">
|
||||
<a-tabs v-model:activeKey="messageType" type="rounded" destroy-on-hide>
|
||||
<a-tab-pane v-for="item in tabList" :key="item.key">
|
||||
<template #title>
|
||||
<span> {{ item.title }}{{ formatUnreadLength(item.key) }} </span>
|
||||
</template>
|
||||
<a-result v-if="!renderList.length" status="404">
|
||||
<template #subtitle> {{ $t('messageBox.noContent') }} </template>
|
||||
</a-result>
|
||||
<List :render-list="renderList" :unread-count="unreadCount" @item-click="handleItemClick" />
|
||||
</a-tab-pane>
|
||||
<template #extra>
|
||||
<a-button type="text" @click="emptyList">
|
||||
{{ $t('messageBox.tab.button') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-tabs>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, toRefs, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { queryMessageList, setMessageStatus, MessageRecord, MessageListType } from '@/api/message'
|
||||
import useLoading from '@/hooks/loading'
|
||||
import List from './list.vue'
|
||||
|
||||
interface TabItem {
|
||||
key: string
|
||||
title: string
|
||||
avatar?: string
|
||||
}
|
||||
const { loading, setLoading } = useLoading(true)
|
||||
const messageType = ref('message')
|
||||
const { t } = useI18n()
|
||||
const messageData = reactive<{
|
||||
renderList: MessageRecord[]
|
||||
messageList: MessageRecord[]
|
||||
}>({
|
||||
renderList: [],
|
||||
messageList: [],
|
||||
})
|
||||
toRefs(messageData)
|
||||
const tabList: TabItem[] = [
|
||||
{
|
||||
key: 'message',
|
||||
title: t('messageBox.tab.title.message'),
|
||||
},
|
||||
{
|
||||
key: 'notice',
|
||||
title: t('messageBox.tab.title.notice'),
|
||||
},
|
||||
{
|
||||
key: 'todo',
|
||||
title: t('messageBox.tab.title.todo'),
|
||||
},
|
||||
]
|
||||
async function fetchSourceData() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const { data } = await queryMessageList()
|
||||
messageData.messageList = data
|
||||
} catch (err) {
|
||||
// you can report use errorHandler or other
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
async function readMessage(data: MessageListType) {
|
||||
const ids = data.map((item) => item.id)
|
||||
await setMessageStatus({ ids })
|
||||
fetchSourceData()
|
||||
}
|
||||
const renderList = computed(() => {
|
||||
return messageData.messageList.filter((item) => messageType.value === item.type)
|
||||
})
|
||||
const unreadCount = computed(() => {
|
||||
return renderList.value.filter((item) => !item.status).length
|
||||
})
|
||||
const getUnreadList = (type: string) => {
|
||||
const list = messageData.messageList.filter((item) => item.type === type && !item.status)
|
||||
return list
|
||||
}
|
||||
const formatUnreadLength = (type: string) => {
|
||||
const list = getUnreadList(type)
|
||||
return list.length ? `(${list.length})` : ``
|
||||
}
|
||||
const handleItemClick = (items: MessageListType) => {
|
||||
if (renderList.value.length) readMessage([...items])
|
||||
}
|
||||
const emptyList = () => {
|
||||
messageData.messageList = []
|
||||
}
|
||||
fetchSourceData()
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
:deep(.arco-popover-popup-content) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.arco-list-item-meta) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
:deep(.arco-tabs-nav) {
|
||||
padding: 14px 0 12px 16px;
|
||||
border-bottom: 1px solid var(--color-neutral-3);
|
||||
}
|
||||
:deep(.arco-tabs-content) {
|
||||
padding-top: 0;
|
||||
.arco-result-subtitle {
|
||||
color: rgb(var(--gray-6));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,142 +0,0 @@
|
||||
<template>
|
||||
<a-list :bordered="false">
|
||||
<a-list-item
|
||||
v-for="item in renderList"
|
||||
:key="item.id"
|
||||
action-layout="vertical"
|
||||
:style="{
|
||||
opacity: item.status ? 0.5 : 1,
|
||||
}"
|
||||
>
|
||||
<template #extra>
|
||||
<a-tag v-if="item.messageType === 0" color="gray">未开始</a-tag>
|
||||
<a-tag v-else-if="item.messageType === 1" color="green">已开通</a-tag>
|
||||
<a-tag v-else-if="item.messageType === 2" color="blue">进行中</a-tag>
|
||||
<a-tag v-else-if="item.messageType === 3" color="red">即将到期</a-tag>
|
||||
</template>
|
||||
<div class="item-wrap" @click="onItemClick(item)">
|
||||
<a-list-item-meta>
|
||||
<template v-if="item.avatar" #avatar>
|
||||
<a-avatar shape="circle">
|
||||
<img v-if="item.avatar" :src="item.avatar" />
|
||||
<icon-desktop v-else />
|
||||
</a-avatar>
|
||||
</template>
|
||||
<template #title>
|
||||
<a-space :size="4">
|
||||
<span>{{ item.title }}</span>
|
||||
<a-typography-text type="secondary">
|
||||
{{ item.subTitle }}
|
||||
</a-typography-text>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #description>
|
||||
<div>
|
||||
<a-typography-paragraph
|
||||
:ellipsis="{
|
||||
rows: 1,
|
||||
}"
|
||||
>{{ item.content }}</a-typography-paragraph
|
||||
>
|
||||
<a-typography-text v-if="item.type === 'message'" class="time-text">
|
||||
{{ item.time }}
|
||||
</a-typography-text>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</div>
|
||||
</a-list-item>
|
||||
<template #footer>
|
||||
<a-space fill :size="0" :class="{ 'add-border-top': renderList.length < showMax }">
|
||||
<div class="footer-wrap">
|
||||
<a-link @click="allRead">{{ $t('messageBox.allRead') }}</a-link>
|
||||
</div>
|
||||
<div class="footer-wrap">
|
||||
<a-link>{{ $t('messageBox.viewMore') }}</a-link>
|
||||
</div>
|
||||
</a-space>
|
||||
</template>
|
||||
<div
|
||||
v-if="renderList.length && renderList.length < 3"
|
||||
:style="{ height: (showMax - renderList.length) * 86 + 'px' }"
|
||||
></div>
|
||||
</a-list>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue'
|
||||
import { MessageRecord, MessageListType } from '@/api/message'
|
||||
|
||||
const props = defineProps({
|
||||
renderList: {
|
||||
type: Array as PropType<MessageListType>,
|
||||
required: true,
|
||||
},
|
||||
unreadCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['itemClick'])
|
||||
const allRead = () => {
|
||||
emit('itemClick', [...props.renderList])
|
||||
}
|
||||
|
||||
const onItemClick = (item: MessageRecord) => {
|
||||
if (!item.status) {
|
||||
emit('itemClick', [item])
|
||||
}
|
||||
}
|
||||
const showMax = 3
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
:deep(.arco-list) {
|
||||
.arco-list-item {
|
||||
min-height: 86px;
|
||||
border-bottom: 1px solid rgb(var(--gray-3));
|
||||
}
|
||||
.arco-list-item-extra {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
}
|
||||
.arco-list-item-meta-content {
|
||||
flex: 1;
|
||||
}
|
||||
.item-wrap {
|
||||
cursor: pointer;
|
||||
}
|
||||
.time-text {
|
||||
font-size: 12px;
|
||||
color: rgb(var(--gray-6));
|
||||
}
|
||||
.arco-empty {
|
||||
display: none;
|
||||
}
|
||||
.arco-list-footer {
|
||||
padding: 0;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
border-top: none;
|
||||
.arco-space-item {
|
||||
width: 100%;
|
||||
border-right: 1px solid rgb(var(--gray-3));
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
.add-border-top {
|
||||
border-top: 1px solid rgb(var(--gray-3));
|
||||
}
|
||||
}
|
||||
.footer-wrap {
|
||||
text-align: center;
|
||||
}
|
||||
.arco-typography {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.add-border {
|
||||
border-top: 1px solid rgb(var(--gray-3));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +0,0 @@
|
||||
export default {
|
||||
'messageBox.tab.title.message': 'Message',
|
||||
'messageBox.tab.title.notice': 'Notice',
|
||||
'messageBox.tab.title.todo': 'Todo',
|
||||
'messageBox.tab.button': 'empty',
|
||||
'messageBox.allRead': 'All Read',
|
||||
'messageBox.viewMore': 'View More',
|
||||
'messageBox.noContent': 'No Content',
|
||||
'messageBox.switchRoles': 'Switch Roles',
|
||||
'messageBox.userCenter': 'User Center',
|
||||
'messageBox.userSettings': 'User Settings',
|
||||
'messageBox.logout': 'Logout',
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export default {
|
||||
'messageBox.tab.title.message': '消息',
|
||||
'messageBox.tab.title.notice': '通知',
|
||||
'messageBox.tab.title.todo': '待办',
|
||||
'messageBox.tab.button': '清空',
|
||||
'messageBox.allRead': '全部已读',
|
||||
'messageBox.viewMore': '查看更多',
|
||||
'messageBox.noContent': '暂无内容',
|
||||
'messageBox.switchRoles': '切换角色',
|
||||
'messageBox.userCenter': '用户中心',
|
||||
'messageBox.userSettings': '用户设置',
|
||||
'messageBox.logout': '登出登录',
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
<template>
|
||||
<div class="navbar">
|
||||
<div class="left-side">
|
||||
<a-space>
|
||||
<img
|
||||
alt="logo"
|
||||
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image"
|
||||
/>
|
||||
<a-typography-title :style="{ margin: 0, fontSize: '18px' }" :heading="5">
|
||||
成都测试管理平台
|
||||
</a-typography-title>
|
||||
<icon-menu-fold
|
||||
v-if="!topMenu && appStore.device === 'mobile'"
|
||||
style="font-size: 22px; cursor: pointer"
|
||||
@click="toggleDrawerMenu"
|
||||
/>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="center-side">
|
||||
<Menu v-if="topMenu" />
|
||||
</div>
|
||||
<ul class="right-side">
|
||||
<li>
|
||||
<a-tooltip :content="$t('settings.search')">
|
||||
<a-button class="nav-btn" type="outline" :shape="'circle'">
|
||||
<template #icon>
|
||||
<icon-search />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<a-tooltip :content="$t('settings.language')">
|
||||
<a-button class="nav-btn" type="outline" :shape="'circle'" @click="setDropDownVisible">
|
||||
<template #icon>
|
||||
<icon-language />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-dropdown trigger="click" @select="changeLocale as any">
|
||||
<div ref="triggerBtn" class="trigger-btn"></div>
|
||||
<template #content>
|
||||
<a-doption v-for="item in locales" :key="item.value" :value="item.value">
|
||||
<template #icon>
|
||||
<icon-check v-show="item.value === currentLocale" />
|
||||
</template>
|
||||
{{ item.label }}
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</li>
|
||||
<li>
|
||||
<a-tooltip
|
||||
:content="
|
||||
theme === 'light' ? $t('settings.navbar.theme.toDark') : $t('settings.navbar.theme.toLight')
|
||||
"
|
||||
>
|
||||
<a-button class="nav-btn" type="outline" :shape="'circle'" @click="handleToggleTheme">
|
||||
<template #icon>
|
||||
<icon-moon-fill v-if="theme === 'dark'" />
|
||||
<icon-sun-fill v-else />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<a-tooltip :content="$t('settings.navbar.alerts')">
|
||||
<div class="message-box-trigger">
|
||||
<a-badge :count="9" dot>
|
||||
<a-button class="nav-btn" type="outline" :shape="'circle'" @click="setPopoverVisible">
|
||||
<icon-notification />
|
||||
</a-button>
|
||||
</a-badge>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-popover
|
||||
trigger="click"
|
||||
:arrow-style="{ display: 'none' }"
|
||||
:content-style="{ padding: 0, minWidth: '400px' }"
|
||||
content-class="message-popover"
|
||||
>
|
||||
<div ref="refBtn" class="ref-btn"></div>
|
||||
<template #content>
|
||||
<message-box />
|
||||
</template>
|
||||
</a-popover>
|
||||
</li>
|
||||
<li>
|
||||
<a-tooltip
|
||||
:content="isFullscreen ? $t('settings.navbar.screen.toExit') : $t('settings.navbar.screen.toFull')"
|
||||
>
|
||||
<a-button class="nav-btn" type="outline" :shape="'circle'" @click="toggleFullScreen">
|
||||
<template #icon>
|
||||
<icon-fullscreen-exit v-if="isFullscreen" />
|
||||
<icon-fullscreen v-else />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<a-tooltip :content="$t('settings.title')">
|
||||
<a-button class="nav-btn" type="outline" :shape="'circle'" @click="setVisible">
|
||||
<template #icon>
|
||||
<icon-settings />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<a-dropdown trigger="click">
|
||||
<a-avatar :size="32" :style="{ marginRight: '8px', cursor: 'pointer' }">
|
||||
<img alt="avatar" :src="avatar" />
|
||||
</a-avatar>
|
||||
<template #content>
|
||||
<a-doption>
|
||||
<a-space @click="switchRoles">
|
||||
<icon-tag />
|
||||
<span>
|
||||
{{ $t('messageBox.switchRoles') }}
|
||||
</span>
|
||||
</a-space>
|
||||
</a-doption>
|
||||
<a-doption>
|
||||
<a-space @click="$router.push({ name: 'Info' })">
|
||||
<icon-user />
|
||||
<span>
|
||||
{{ $t('messageBox.userCenter') }}
|
||||
</span>
|
||||
</a-space>
|
||||
</a-doption>
|
||||
<a-doption>
|
||||
<a-space @click="$router.push({ name: 'Setting' })">
|
||||
<icon-settings />
|
||||
<span>
|
||||
{{ $t('messageBox.userSettings') }}
|
||||
</span>
|
||||
</a-space>
|
||||
</a-doption>
|
||||
<a-doption>
|
||||
<a-space @click="handleLogout">
|
||||
<icon-export />
|
||||
<span>
|
||||
{{ $t('messageBox.logout') }}
|
||||
</span>
|
||||
</a-space>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, inject } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { useDark, useToggle, useFullscreen } from '@vueuse/core'
|
||||
import { useAppStore, useUserStore } from '@/store'
|
||||
import { LOCALE_OPTIONS } from '@/locale'
|
||||
import useLocale from '@/hooks/locale'
|
||||
import useUser from '@/hooks/user'
|
||||
import Menu from '@/components/menu/index.vue'
|
||||
import MessageBox from '../message-box/index.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const userStore = useUserStore()
|
||||
const { logout } = useUser()
|
||||
const { changeLocale, currentLocale } = useLocale()
|
||||
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen()
|
||||
const locales = [...LOCALE_OPTIONS]
|
||||
const avatar = computed(() => {
|
||||
return userStore.avatar
|
||||
})
|
||||
const theme = computed(() => {
|
||||
return appStore.theme
|
||||
})
|
||||
const topMenu = computed(() => appStore.topMenu && appStore.menu)
|
||||
const isDark = useDark({
|
||||
selector: 'body',
|
||||
attribute: 'arco-theme',
|
||||
valueDark: 'dark',
|
||||
valueLight: 'light',
|
||||
storageKey: 'arco-theme',
|
||||
onChanged(dark: boolean) {
|
||||
// overridden default behavior
|
||||
appStore.toggleTheme(dark)
|
||||
},
|
||||
})
|
||||
const toggleTheme = useToggle(isDark)
|
||||
const handleToggleTheme = () => {
|
||||
toggleTheme()
|
||||
}
|
||||
const setVisible = () => {
|
||||
appStore.updateSettings({ globalSettings: true })
|
||||
}
|
||||
const refBtn = ref()
|
||||
const triggerBtn = ref()
|
||||
const setPopoverVisible = () => {
|
||||
const event = new MouseEvent('click', {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
refBtn.value.dispatchEvent(event)
|
||||
}
|
||||
const handleLogout = () => {
|
||||
logout()
|
||||
}
|
||||
const setDropDownVisible = () => {
|
||||
const event = new MouseEvent('click', {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
triggerBtn.value.dispatchEvent(event)
|
||||
}
|
||||
const switchRoles = async () => {
|
||||
const res = await userStore.switchRoles()
|
||||
Message.success(res as string)
|
||||
}
|
||||
const toggleDrawerMenu = inject('toggleDrawerMenu') as () => void
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
background-color: var(--color-bg-2);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.left-side {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.center-side {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.right-side {
|
||||
display: flex;
|
||||
padding-right: 20px;
|
||||
list-style: none;
|
||||
:deep(.locale-select) {
|
||||
border-radius: 20px;
|
||||
}
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-text-1);
|
||||
text-decoration: none;
|
||||
}
|
||||
.nav-btn {
|
||||
border-color: rgb(var(--gray-2));
|
||||
color: rgb(var(--gray-8));
|
||||
font-size: 16px;
|
||||
}
|
||||
.trigger-btn,
|
||||
.ref-btn {
|
||||
position: absolute;
|
||||
bottom: 14px;
|
||||
}
|
||||
.trigger-btn {
|
||||
margin-left: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.message-popover {
|
||||
.arco-popover-content {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,61 +0,0 @@
|
||||
<template>
|
||||
<div class="modal">
|
||||
<a-modal v-model:visible="dialogVisible" @before-ok="handleOkBtn" @cancel="handleCancelBtn">
|
||||
<template #title> {{ props.title }} </template>
|
||||
<div class="content">
|
||||
<chen-form v-model="formData" v-bind="modelConfig"></chen-form>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, defineProps, defineExpose } from 'vue'
|
||||
import chenForm from '@/base-ui/from'
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '新建或编辑',
|
||||
},
|
||||
modelConfig: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
defaultInfo: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
})
|
||||
// 绑定数据在这里
|
||||
const formData = ref<any>({})
|
||||
const dialogVisible = ref(false)
|
||||
const handleOkBtn = (done: any) => {
|
||||
console.log(formData.value)
|
||||
console.log('点击了ok')
|
||||
// 这里发生异步请求时关闭
|
||||
setTimeout(() => {
|
||||
done()
|
||||
}, 1000)
|
||||
}
|
||||
const handleCancelBtn = () => {
|
||||
console.log('点击了取消')
|
||||
}
|
||||
// 监听父组件编辑,编辑回显
|
||||
watch(
|
||||
() => props.defaultInfo,
|
||||
(newValue) => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const item of props.modelConfig.formItems) {
|
||||
formData.value[item.field] = newValue[item.field]
|
||||
}
|
||||
}
|
||||
)
|
||||
// 暴露自己的显示隐藏变量
|
||||
defineExpose({
|
||||
dialogVisible,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,90 +0,0 @@
|
||||
<template>
|
||||
<div class="tab-bar-container">
|
||||
<a-affix ref="affixRef" :offset-top="offsetTop">
|
||||
<div class="tab-bar-box">
|
||||
<div class="tab-bar-scroll">
|
||||
<div class="tags-wrap">
|
||||
<tab-item v-for="(tag, index) in tagList" :key="tag.fullPath" :index="index" :item-data="tag" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag-bar-operation"></div>
|
||||
</div>
|
||||
</a-affix>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onUnmounted } from 'vue'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import { listenerRouteChange, removeRouteListener } from '@/utils/route-listener'
|
||||
import { useAppStore, useTabBarStore } from '@/store'
|
||||
import tabItem from './tab-item.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const tabBarStore = useTabBarStore()
|
||||
|
||||
const affixRef = ref()
|
||||
const tagList = computed(() => {
|
||||
return tabBarStore.getTabList
|
||||
})
|
||||
const offsetTop = computed(() => {
|
||||
return appStore.navbar ? 60 : 0
|
||||
})
|
||||
|
||||
watch(
|
||||
() => appStore.navbar,
|
||||
() => {
|
||||
affixRef.value.updatePosition()
|
||||
}
|
||||
)
|
||||
listenerRouteChange((route: RouteLocationNormalized) => {
|
||||
if (!route.meta.noAffix && !tagList.value.some((tag) => tag.fullPath === route.fullPath)) {
|
||||
tabBarStore.updateTabList(route)
|
||||
}
|
||||
}, true)
|
||||
|
||||
onUnmounted(() => {
|
||||
removeRouteListener()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.tab-bar-container {
|
||||
position: relative;
|
||||
background-color: var(--color-bg-2);
|
||||
.tab-bar-box {
|
||||
display: flex;
|
||||
padding: 0 0 0 20px;
|
||||
background-color: var(--color-bg-2);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
.tab-bar-scroll {
|
||||
height: 32px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
.tags-wrap {
|
||||
padding: 4px 0;
|
||||
height: 48px;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
|
||||
:deep(.arco-tag) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
&:first-child {
|
||||
.arco-tag-close-btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tag-bar-operation {
|
||||
width: 100px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +0,0 @@
|
||||
## 组件说明
|
||||
|
||||
该组件非官方最终设计规范,以单独组件存在。
|
||||
|
||||
同时仅仅提供最基本的功能,后续进行优化及更改。
|
||||
|
||||
## Component description
|
||||
|
||||
The component unofficial final design specification exists as a separate component.
|
||||
|
||||
At the same time, only the most basic functions are provided, and subsequent optimizations and changes will be made.
|
||||
@@ -1,188 +0,0 @@
|
||||
<template>
|
||||
<a-dropdown trigger="contextMenu" :popup-max-height="false" @select="actionSelect">
|
||||
<span
|
||||
class="arco-tag arco-tag-size-medium arco-tag-checked"
|
||||
:class="{ 'link-activated': itemData.fullPath === $route.fullPath }"
|
||||
@click="goto(itemData)"
|
||||
>
|
||||
<span class="tag-link">
|
||||
{{ $t(itemData.title) }}
|
||||
</span>
|
||||
<span
|
||||
class="arco-icon-hover arco-tag-icon-hover arco-icon-hover-size-medium arco-tag-close-btn"
|
||||
@click.stop="tagClose(itemData, index)"
|
||||
>
|
||||
<icon-close />
|
||||
</span>
|
||||
</span>
|
||||
<template #content>
|
||||
<a-doption :disabled="disabledReload" :value="Eaction.reload">
|
||||
<icon-refresh />
|
||||
<span>重新加载</span>
|
||||
</a-doption>
|
||||
<a-doption class="sperate-line" :disabled="disabledCurrent" :value="Eaction.current">
|
||||
<icon-close />
|
||||
<span>关闭当前标签页</span>
|
||||
</a-doption>
|
||||
<a-doption :disabled="disabledLeft" :value="Eaction.left">
|
||||
<icon-to-left />
|
||||
<span>关闭左侧标签页</span>
|
||||
</a-doption>
|
||||
<a-doption class="sperate-line" :disabled="disabledRight" :value="Eaction.right">
|
||||
<icon-to-right />
|
||||
<span>关闭右侧标签页</span>
|
||||
</a-doption>
|
||||
<a-doption :value="Eaction.others">
|
||||
<icon-swap />
|
||||
<span>关闭其它标签页</span>
|
||||
</a-doption>
|
||||
<a-doption :value="Eaction.all">
|
||||
<icon-folder-delete />
|
||||
<span>关闭全部标签页</span>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useTabBarStore } from '@/store'
|
||||
import type { TagProps } from '@/store/modules/tab-bar/types'
|
||||
import { DEFAULT_ROUTE_NAME, REDIRECT_ROUTE_NAME } from '@/router/constants'
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
enum Eaction {
|
||||
reload = 'reload',
|
||||
current = 'current',
|
||||
left = 'left',
|
||||
right = 'right',
|
||||
others = 'others',
|
||||
all = 'all',
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
itemData: {
|
||||
type: Object as PropType<TagProps>,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const tabBarStore = useTabBarStore()
|
||||
|
||||
const goto = (tag: TagProps) => {
|
||||
router.push({ ...tag })
|
||||
}
|
||||
const tagList = computed(() => {
|
||||
return tabBarStore.getTabList
|
||||
})
|
||||
|
||||
const disabledReload = computed(() => {
|
||||
return props.itemData.fullPath !== route.fullPath
|
||||
})
|
||||
|
||||
const disabledCurrent = computed(() => {
|
||||
return props.index === 0
|
||||
})
|
||||
|
||||
const disabledLeft = computed(() => {
|
||||
return [0, 1].includes(props.index)
|
||||
})
|
||||
|
||||
const disabledRight = computed(() => {
|
||||
return props.index === tagList.value.length - 1
|
||||
})
|
||||
|
||||
const tagClose = (tag: TagProps, idx: number) => {
|
||||
tabBarStore.deleteTag(idx, tag)
|
||||
if (props.itemData.fullPath === route.fullPath) {
|
||||
const latest = tagList.value[idx - 1] // 获取队列的前一个tab
|
||||
router.push({ name: latest.name })
|
||||
}
|
||||
}
|
||||
|
||||
const findCurrentRouteIndex = () => {
|
||||
return tagList.value.findIndex((el) => el.fullPath === route.fullPath)
|
||||
}
|
||||
const actionSelect = async (value: any) => {
|
||||
const { itemData, index } = props
|
||||
const copyTagList = [...tagList.value]
|
||||
if (value === Eaction.current) {
|
||||
tagClose(itemData, index)
|
||||
} else if (value === Eaction.left) {
|
||||
const currentRouteIdx = findCurrentRouteIndex()
|
||||
copyTagList.splice(1, props.index - 1)
|
||||
|
||||
tabBarStore.freshTabList(copyTagList)
|
||||
if (currentRouteIdx < index) {
|
||||
router.push({ name: itemData.name })
|
||||
}
|
||||
} else if (value === Eaction.right) {
|
||||
const currentRouteIdx = findCurrentRouteIndex()
|
||||
copyTagList.splice(props.index + 1)
|
||||
|
||||
tabBarStore.freshTabList(copyTagList)
|
||||
if (currentRouteIdx > index) {
|
||||
router.push({ name: itemData.name })
|
||||
}
|
||||
} else if (value === Eaction.others) {
|
||||
const filterList = tagList.value.filter((el, idx) => {
|
||||
return idx === 0 || idx === props.index
|
||||
})
|
||||
tabBarStore.freshTabList(filterList)
|
||||
router.push({ name: itemData.name })
|
||||
} else if (value === Eaction.reload) {
|
||||
tabBarStore.deleteCache(itemData)
|
||||
await router.push({
|
||||
name: REDIRECT_ROUTE_NAME,
|
||||
params: {
|
||||
path: route.fullPath,
|
||||
},
|
||||
})
|
||||
tabBarStore.addCache(itemData.name)
|
||||
} else {
|
||||
tabBarStore.resetTabList()
|
||||
router.push({ name: DEFAULT_ROUTE_NAME })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.tag-link {
|
||||
color: var(--color-text-2);
|
||||
text-decoration: none;
|
||||
}
|
||||
.link-activated {
|
||||
color: rgb(var(--link-6));
|
||||
.tag-link {
|
||||
color: rgb(var(--link-6));
|
||||
}
|
||||
& + .arco-tag-close-btn {
|
||||
color: rgb(var(--link-6));
|
||||
}
|
||||
}
|
||||
:deep(.arco-dropdown-option-content) {
|
||||
span {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.arco-dropdown-open {
|
||||
.tag-link {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
.arco-tag-close-btn {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
}
|
||||
.sperate-line {
|
||||
border-bottom: 1px solid var(--color-neutral-3);
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"theme": "light",
|
||||
"colorWeak": false,
|
||||
"navbar": true,
|
||||
"menu": true,
|
||||
"topMenu": false,
|
||||
"hideMenu": false,
|
||||
"menuCollapse": false,
|
||||
"footer": true,
|
||||
"themeColor": "#165DFF",
|
||||
"menuWidth": 220,
|
||||
"globalSettings": false,
|
||||
"device": "desktop",
|
||||
"tabBar": true,
|
||||
"menuFromServer": false,
|
||||
"serverMenu": []
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { App } from 'vue'
|
||||
import permission from './permission'
|
||||
|
||||
export default {
|
||||
install(Vue: App) {
|
||||
Vue.directive('permission', permission)
|
||||
},
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { DirectiveBinding } from 'vue'
|
||||
import { useUserStore } from '@/store'
|
||||
|
||||
function checkPermission(el: HTMLElement, binding: DirectiveBinding) {
|
||||
const { value } = binding
|
||||
const userStore = useUserStore()
|
||||
const { role } = userStore
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length > 0) {
|
||||
const permissionValues = value
|
||||
|
||||
const hasPermission = permissionValues.includes(role)
|
||||
if (!hasPermission && el.parentNode) {
|
||||
el.parentNode.removeChild(el)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error(`没有权限! 该权限为 v-permission="['admin','user']"`)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||
checkPermission(el, binding)
|
||||
},
|
||||
updated(el: HTMLElement, binding: DirectiveBinding) {
|
||||
checkPermission(el, binding)
|
||||
},
|
||||
}
|
||||
11
cdtestplant/src/env.d.ts
vendored
11
cdtestplant/src/env.d.ts
vendored
@@ -1,11 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_BASE_URL: string
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { computed } from 'vue'
|
||||
import { EChartsOption } from 'echarts'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
// for code hints
|
||||
// import { SeriesOption } from 'echarts';
|
||||
// Because there are so many configuration items, this provides a relatively convenient code hint.
|
||||
// When using vue, pay attention to the reactive issues. It is necessary to ensure that corresponding functions can be triggered, TypeScript does not report errors, and code writing is convenient.
|
||||
interface optionsFn {
|
||||
(isDark: boolean): EChartsOption
|
||||
}
|
||||
|
||||
export default function useChartOption(sourceOption: optionsFn) {
|
||||
const appStore = useAppStore()
|
||||
const isDark = computed(() => {
|
||||
return appStore.theme === 'dark'
|
||||
})
|
||||
// echarts support https://echarts.apache.org/zh/theme-builder.html
|
||||
// It's not used here
|
||||
// TODO echarts themes
|
||||
const chartOption = computed<EChartsOption>(() => {
|
||||
return sourceOption(isDark.value)
|
||||
})
|
||||
return {
|
||||
chartOption,
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
export default function useLoading(initValue = false) {
|
||||
const loading = ref(initValue)
|
||||
const setLoading = (value: boolean) => {
|
||||
loading.value = value
|
||||
}
|
||||
const toggle = () => {
|
||||
loading.value = !loading.value
|
||||
}
|
||||
return {
|
||||
loading,
|
||||
setLoading,
|
||||
toggle,
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
|
||||
export default function useLocale() {
|
||||
const i18 = useI18n()
|
||||
const currentLocale = computed(() => {
|
||||
return i18.locale.value
|
||||
})
|
||||
const changeLocale = (value: string) => {
|
||||
if (i18.locale.value === value) {
|
||||
return
|
||||
}
|
||||
i18.locale.value = value
|
||||
localStorage.setItem('arco-locale', value)
|
||||
Message.success(i18.t('navbar.action.locale'))
|
||||
}
|
||||
return {
|
||||
currentLocale,
|
||||
changeLocale,
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'
|
||||
import { useUserStore } from '@/store'
|
||||
|
||||
export default function usePermission() {
|
||||
const userStore = useUserStore()
|
||||
return {
|
||||
// 返回的函数可自定义路由权限
|
||||
accessRouter(route: RouteLocationNormalized | RouteRecordRaw) {
|
||||
return (
|
||||
!route.meta?.requiresAuth ||
|
||||
!route.meta?.roles ||
|
||||
route.meta?.roles?.includes('*') ||
|
||||
route.meta?.roles?.includes(userStore.role)
|
||||
)
|
||||
},
|
||||
findFirstPermissionRoute(_routers: any, role = 'admin') {
|
||||
const cloneRouters = [..._routers]
|
||||
while (cloneRouters.length) {
|
||||
const firstElement = cloneRouters.shift()
|
||||
if (
|
||||
firstElement?.meta?.roles?.find((el: string[]) => {
|
||||
return el.includes('*') || el.includes(role)
|
||||
})
|
||||
)
|
||||
return { name: firstElement.name }
|
||||
if (firstElement?.children) {
|
||||
cloneRouters.push(...firstElement.children)
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
// 你可以添加任何规则
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { ref, UnwrapRef } from 'vue'
|
||||
import { AxiosResponse } from 'axios'
|
||||
import { HttpResponse } from '@/api/interceptor'
|
||||
import useLoading from './loading'
|
||||
|
||||
// use to fetch list
|
||||
// Don't use async function. It doesn't work in async function.
|
||||
// Use the bind function to add parameters
|
||||
// example: useRequest(api.bind(null, {}))
|
||||
|
||||
export default function useRequest<T>(
|
||||
api: () => Promise<AxiosResponse<HttpResponse>>,
|
||||
defaultValue = [] as unknown as T,
|
||||
isLoading = true
|
||||
) {
|
||||
const { loading, setLoading } = useLoading(isLoading)
|
||||
const response = ref<T>(defaultValue)
|
||||
api()
|
||||
.then((res) => {
|
||||
response.value = res.data as unknown as UnwrapRef<T>
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
return { loading, response }
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { onMounted, onBeforeMount, onBeforeUnmount } from 'vue'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { useAppStore } from '@/store'
|
||||
import { addEventListen, removeEventListen } from '@/utils/event'
|
||||
|
||||
const WIDTH = 992 // https://arco.design/vue/component/grid#responsivevalue
|
||||
|
||||
function queryDevice() {
|
||||
const rect = document.body.getBoundingClientRect()
|
||||
return rect.width - 1 < WIDTH
|
||||
}
|
||||
|
||||
export default function useResponsive(immediate?: boolean) {
|
||||
const appStore = useAppStore()
|
||||
function resizeHandler() {
|
||||
if (!document.hidden) {
|
||||
const isMobile = queryDevice()
|
||||
appStore.toggleDevice(isMobile ? 'mobile' : 'desktop')
|
||||
appStore.toggleMenu(isMobile)
|
||||
}
|
||||
}
|
||||
const debounceFn = useDebounceFn(resizeHandler, 100)
|
||||
onMounted(() => {
|
||||
if (immediate) debounceFn()
|
||||
})
|
||||
onBeforeMount(() => {
|
||||
addEventListen(window, 'resize', debounceFn)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
removeEventListen(window, 'resize', debounceFn)
|
||||
})
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { computed } from 'vue'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
export default function useThemes() {
|
||||
const appStore = useAppStore()
|
||||
const isDark = computed(() => {
|
||||
return appStore.theme === 'dark'
|
||||
})
|
||||
return {
|
||||
isDark,
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
|
||||
import { useUserStore } from '@/store'
|
||||
|
||||
export default function useUser() {
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const logout = async (logoutTo?: string) => {
|
||||
await userStore.logout()
|
||||
const currentRoute = router.currentRoute.value
|
||||
Message.success('登出成功')
|
||||
router.push({
|
||||
name: logoutTo && typeof logoutTo === 'string' ? logoutTo : 'login',
|
||||
query: {
|
||||
...router.currentRoute.value.query,
|
||||
redirect: currentRoute.name as string,
|
||||
},
|
||||
})
|
||||
}
|
||||
return {
|
||||
logout,
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
export default function useVisible(initValue = false) {
|
||||
const visible = ref(initValue)
|
||||
const setVisible = (value: boolean) => {
|
||||
visible.value = value
|
||||
}
|
||||
const toggle = () => {
|
||||
visible.value = !visible.value
|
||||
}
|
||||
return {
|
||||
visible,
|
||||
setVisible,
|
||||
toggle,
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
<template>
|
||||
<a-layout class="layout" :class="{ mobile: appStore.hideMenu }">
|
||||
<div v-if="navbar" class="layout-navbar">
|
||||
<NavBar />
|
||||
</div>
|
||||
<a-layout>
|
||||
<a-layout>
|
||||
<a-layout-sider
|
||||
v-if="renderMenu"
|
||||
v-show="!hideMenu"
|
||||
class="layout-sider"
|
||||
breakpoint="xl"
|
||||
:collapsed="collapsed"
|
||||
:collapsible="true"
|
||||
:width="menuWidth"
|
||||
:style="{ paddingTop: navbar ? '60px' : '' }"
|
||||
:hide-trigger="true"
|
||||
@collapse="setCollapsed"
|
||||
>
|
||||
<div class="menu-wrapper">
|
||||
<Menu />
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
<a-drawer
|
||||
v-if="hideMenu"
|
||||
:visible="drawerVisible"
|
||||
placement="left"
|
||||
:footer="false"
|
||||
mask-closable
|
||||
:closable="false"
|
||||
@cancel="drawerCancel"
|
||||
>
|
||||
<Menu />
|
||||
</a-drawer>
|
||||
<a-layout class="layout-content" :style="paddingStyle">
|
||||
<TabBar v-if="appStore.tabBar" />
|
||||
<a-layout-content>
|
||||
<PageLayout />
|
||||
</a-layout-content>
|
||||
<Footer v-if="footer" />
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, provide, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAppStore, useUserStore } from '@/store'
|
||||
import NavBar from '@/components/navbar/index.vue'
|
||||
import Menu from '@/components/menu/index.vue'
|
||||
import Footer from '@/components/footer/index.vue'
|
||||
import TabBar from '@/components/tab-bar/index.vue'
|
||||
import usePermission from '@/hooks/permission'
|
||||
import useResponsive from '@/hooks/responsive'
|
||||
import PageLayout from './page-layout.vue'
|
||||
|
||||
const isInit = ref(false)
|
||||
const appStore = useAppStore()
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const permission = usePermission()
|
||||
useResponsive(true)
|
||||
const navbarHeight = `60px`
|
||||
const navbar = computed(() => appStore.navbar)
|
||||
const renderMenu = computed(() => appStore.menu && !appStore.topMenu)
|
||||
const hideMenu = computed(() => appStore.hideMenu)
|
||||
const footer = computed(() => appStore.footer)
|
||||
const menuWidth = computed(() => {
|
||||
return appStore.menuCollapse ? 48 : appStore.menuWidth
|
||||
})
|
||||
const collapsed = computed(() => {
|
||||
return appStore.menuCollapse
|
||||
})
|
||||
const paddingStyle = computed(() => {
|
||||
const paddingLeft = renderMenu.value && !hideMenu.value ? { paddingLeft: `${menuWidth.value}px` } : {}
|
||||
const paddingTop = navbar.value ? { paddingTop: navbarHeight } : {}
|
||||
return { ...paddingLeft, ...paddingTop }
|
||||
})
|
||||
const setCollapsed = (val: boolean) => {
|
||||
if (!isInit.value) return // for page initialization menu state problem
|
||||
appStore.updateSettings({ menuCollapse: val })
|
||||
}
|
||||
watch(
|
||||
() => userStore.role,
|
||||
(roleValue) => {
|
||||
if (roleValue && !permission.accessRouter(route)) router.push({ name: 'notFound' })
|
||||
}
|
||||
)
|
||||
const drawerVisible = ref(false)
|
||||
const drawerCancel = () => {
|
||||
drawerVisible.value = false
|
||||
}
|
||||
provide('toggleDrawerMenu', () => {
|
||||
drawerVisible.value = !drawerVisible.value
|
||||
})
|
||||
onMounted(() => {
|
||||
isInit.value = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@nav-size-height: 60px;
|
||||
@layout-max-width: 1100px;
|
||||
|
||||
.layout {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.layout-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
height: @nav-size-height;
|
||||
}
|
||||
|
||||
.layout-sider {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 99;
|
||||
height: 100%;
|
||||
transition: all 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -1px;
|
||||
display: block;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background-color: var(--color-border);
|
||||
content: '';
|
||||
}
|
||||
|
||||
> :deep(.arco-layout-sider-children) {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-wrapper {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
:deep(.arco-menu) {
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border: 4px solid transparent;
|
||||
background-clip: padding-box;
|
||||
border-radius: 7px;
|
||||
background-color: var(--color-text-4);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
min-height: 100vh;
|
||||
overflow-y: hidden;
|
||||
background-color: var(--color-fill-2);
|
||||
transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
||||
}
|
||||
</style>
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition name="fade" mode="out-in" appear>
|
||||
<component :is="Component" v-if="route.meta.ignoreCache" :key="route.fullPath" />
|
||||
<keep-alive v-else :include="cacheList">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { useTabBarStore } from '@/store'
|
||||
|
||||
const tabBarStore = useTabBarStore()
|
||||
|
||||
const cacheList = computed(() => tabBarStore.getCacheList)
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
@@ -1,34 +0,0 @@
|
||||
import localeMessageBox from '@/components/message-box/locale/en-US'
|
||||
import localeLogin from '@/views/login/locale/en-US'
|
||||
|
||||
import localeWorkplace from '@/views/dashboard/workplace/locale/en-US'
|
||||
import localeUserManage from '@/views/userAbout/userManage/locale/en-US'
|
||||
import localeSettings from './en-US/settings'
|
||||
|
||||
export default {
|
||||
'menu.dashboard': 'Dashboard',
|
||||
// 开发者添加
|
||||
'menu.userAbout': 'userAbout',
|
||||
'menu.userAbout.manage': 'userManage',
|
||||
'menu.dashboard.monitor': 'theHoleState',
|
||||
'menu.server.dashboard': 'Dashboard-Server',
|
||||
'menu.server.workplace': 'Workplace-Server',
|
||||
'menu.server.monitor': 'Monitor-Server',
|
||||
'menu.list': 'List',
|
||||
'menu.result': 'Result',
|
||||
'menu.exception': 'Exception',
|
||||
'menu.form': 'Form',
|
||||
'menu.profile': 'Profile',
|
||||
'menu.visualization': 'Data Visualization',
|
||||
'menu.user': 'User Center',
|
||||
// 'menu.arcoWebsite': 'Arco Design',
|
||||
// 'menu.faq': 'FAQ',
|
||||
'navbar.docs': 'Docs',
|
||||
'navbar.action.locale': 'Switch to English',
|
||||
...localeSettings,
|
||||
...localeMessageBox,
|
||||
...localeLogin,
|
||||
...localeWorkplace,
|
||||
// ADD
|
||||
...localeUserManage,
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
export default {
|
||||
'settings.title': 'Settings',
|
||||
'settings.themeColor': 'Theme Color',
|
||||
'settings.content': 'Content Setting',
|
||||
'settings.search': 'Search',
|
||||
'settings.language': 'Language',
|
||||
'settings.navbar': 'Navbar',
|
||||
'settings.menuWidth': 'Menu Width (px)',
|
||||
'settings.navbar.theme.toLight': 'Click to use light mode',
|
||||
'settings.navbar.theme.toDark': 'Click to use dark mode',
|
||||
'settings.navbar.screen.toFull': 'Click to switch to full screen mode',
|
||||
'settings.navbar.screen.toExit': 'Click to exit the full screen mode',
|
||||
'settings.navbar.alerts': 'alerts',
|
||||
'settings.menu': 'Menu',
|
||||
'settings.topMenu': 'Top Menu',
|
||||
'settings.tabBar': 'Tab Bar',
|
||||
'settings.footer': 'Footer',
|
||||
'settings.otherSettings': 'Other Settings',
|
||||
'settings.colorWeak': 'Color Weak',
|
||||
'settings.alertContent':
|
||||
'After the configuration is only temporarily effective, if you want to really affect the project, click the "Copy Settings" button below and replace the configuration in settings.json.',
|
||||
'settings.copySettings': 'Copy Settings',
|
||||
'settings.copySettings.message': 'Copy succeeded, please paste to file src/settings.json.',
|
||||
'settings.close': 'Close',
|
||||
'settings.color.tooltip': '10 gradient colors generated according to the theme color',
|
||||
'settings.menuFromServer': 'Menu From Server',
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import en from './en-US'
|
||||
import cn from './zh-CN'
|
||||
|
||||
export const LOCALE_OPTIONS = [
|
||||
{ label: '中文', value: 'zh-CN' },
|
||||
{ label: 'English', value: 'en-US' },
|
||||
]
|
||||
const defaultLocale = localStorage.getItem('arco-locale') || 'zh-CN'
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: defaultLocale,
|
||||
fallbackLocale: 'en-US',
|
||||
legacy: false,
|
||||
allowComposition: true,
|
||||
messages: {
|
||||
'en-US': en,
|
||||
'zh-CN': cn,
|
||||
},
|
||||
})
|
||||
|
||||
export default i18n
|
||||
@@ -1,37 +0,0 @@
|
||||
import localeMessageBox from '@/components/message-box/locale/zh-CN'
|
||||
import localeLogin from '@/views/login/locale/zh-CN'
|
||||
|
||||
import localeWorkplace from '@/views/dashboard/workplace/locale/zh-CN'
|
||||
import localeUserManage from '@/views/userAbout/userManage/locale/zh-CN'
|
||||
|
||||
import localeSettings from './zh-CN/settings'
|
||||
|
||||
export default {
|
||||
'menu.dashboard': '仪表盘',
|
||||
// 框架外添加
|
||||
'menu.userAbout': '用户相关',
|
||||
'menu.userAbout.manage': '用户管理',
|
||||
'menu.userAbout.test': '测试页面',
|
||||
// ~~~~~
|
||||
'menu.dashboard.monitor': '整体概况',
|
||||
'menu.server.dashboard': '仪表盘-服务端',
|
||||
'menu.server.workplace': '工作台-服务端',
|
||||
'menu.server.monitor': '实时监控-服务端',
|
||||
'menu.list': '列表页',
|
||||
'menu.result': '结果页',
|
||||
'menu.exception': '异常页',
|
||||
'menu.form': '表单页',
|
||||
'menu.profile': '详情页',
|
||||
'menu.visualization': '数据可视化',
|
||||
'menu.user': '个人中心',
|
||||
// 'menu.arcoWebsite': 'Arco Design',
|
||||
// 'menu.faq': '常见问题',
|
||||
'navbar.docs': '文档中心',
|
||||
'navbar.action.locale': '切换为中文',
|
||||
...localeSettings,
|
||||
...localeMessageBox,
|
||||
...localeLogin,
|
||||
...localeWorkplace,
|
||||
// add
|
||||
...localeUserManage,
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
export default {
|
||||
'settings.title': '页面配置',
|
||||
'settings.themeColor': '主题色',
|
||||
'settings.content': '内容区域',
|
||||
'settings.search': '搜索',
|
||||
'settings.language': '语言',
|
||||
'settings.navbar': '导航栏',
|
||||
'settings.menuWidth': '菜单宽度 (px)',
|
||||
'settings.navbar.theme.toLight': '点击切换为亮色模式',
|
||||
'settings.navbar.theme.toDark': '点击切换为暗黑模式',
|
||||
'settings.navbar.screen.toFull': '点击切换全屏模式',
|
||||
'settings.navbar.screen.toExit': '点击退出全屏模式',
|
||||
'settings.navbar.alerts': '消息通知',
|
||||
'settings.menu': '菜单栏',
|
||||
'settings.topMenu': '顶部菜单栏',
|
||||
'settings.tabBar': '多页签',
|
||||
'settings.footer': '底部',
|
||||
'settings.otherSettings': '其他设置',
|
||||
'settings.colorWeak': '色弱模式',
|
||||
'settings.alertContent':
|
||||
'配置之后仅是临时生效,要想真正作用于项目,点击下方的 "复制配置" 按钮,将配置替换到 settings.json 中即可。',
|
||||
'settings.copySettings': '复制配置',
|
||||
'settings.copySettings.message': '复制成功,请粘贴到 src/settings.json 文件中',
|
||||
'settings.close': '关闭',
|
||||
'settings.color.tooltip':
|
||||
'根据主题颜色生成的 10 个梯度色(将配置复制到项目中,主题色才能对亮色 / 暗黑模式同时生效)',
|
||||
'settings.menuFromServer': '菜单来源于后台',
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { createApp } from 'vue'
|
||||
import ArcoVue from '@arco-design/web-vue'
|
||||
import ArcoVueIcon from '@arco-design/web-vue/es/icon'
|
||||
import globalComponents from '@/components'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import i18n from './locale'
|
||||
import directive from './directive'
|
||||
import './mock'
|
||||
import App from './App.vue'
|
||||
// 样式通过 arco-plugin 插件导入。详见目录文件 config/plugin/arcoStyleImport.ts
|
||||
// https://arco.design/docs/designlab/use-theme-package
|
||||
import '@/assets/style/global.less'
|
||||
import '@/api/interceptor'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(ArcoVue, {})
|
||||
app.use(ArcoVueIcon)
|
||||
|
||||
app.use(router)
|
||||
app.use(store)
|
||||
app.use(i18n)
|
||||
app.use(globalComponents)
|
||||
app.use(directive)
|
||||
|
||||
app.mount('#app')
|
||||
@@ -1,11 +0,0 @@
|
||||
import Mock from 'mockjs'
|
||||
|
||||
import './user'
|
||||
import './message-box'
|
||||
import './userAbout'
|
||||
|
||||
import '@/views/dashboard/workplace/mock'
|
||||
|
||||
Mock.setup({
|
||||
timeout: '600-1000',
|
||||
})
|
||||
@@ -1,82 +0,0 @@
|
||||
import Mock from 'mockjs'
|
||||
import setupMock, { successResponseWrap } from '@/utils/setup-mock'
|
||||
|
||||
const haveReadIds: number[] = []
|
||||
const getMessageList = () => {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
type: 'message',
|
||||
title: '郑曦月',
|
||||
subTitle: '的私信',
|
||||
avatar: '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/8361eeb82904210b4f55fab888fe8416.png~tplv-uwbnlip3yd-webp.webp',
|
||||
content: '审批请求已发送,请查收',
|
||||
time: '今天 12:30:01',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'message',
|
||||
title: '宁波',
|
||||
subTitle: '的回复',
|
||||
avatar: '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
|
||||
content: '此处 bug 已经修复',
|
||||
time: '今天 12:30:01',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'message',
|
||||
title: '宁波',
|
||||
subTitle: '的回复',
|
||||
avatar: '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
|
||||
content: '此处 bug 已经修复',
|
||||
time: '今天 12:20:01',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: 'notice',
|
||||
title: '续费通知',
|
||||
subTitle: '',
|
||||
avatar: '',
|
||||
content: '您的产品使用期限即将截止,如需继续使用产品请前往购…',
|
||||
time: '今天 12:20:01',
|
||||
messageType: 3,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
type: 'notice',
|
||||
title: '规则开通成功',
|
||||
subTitle: '',
|
||||
avatar: '',
|
||||
content: '内容屏蔽规则于 2021-12-01 开通成功并生效',
|
||||
time: '今天 12:20:01',
|
||||
messageType: 1,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
type: 'todo',
|
||||
title: '质检队列变更',
|
||||
subTitle: '',
|
||||
avatar: '',
|
||||
content: '内容质检队列于 2021-12-01 19:50:23 进行变更,请重新…',
|
||||
time: '今天 12:20:01',
|
||||
messageType: 0,
|
||||
},
|
||||
].map((item) => ({
|
||||
...item,
|
||||
status: haveReadIds.indexOf(item.id) === -1 ? 0 : 1,
|
||||
}))
|
||||
}
|
||||
|
||||
setupMock({
|
||||
setup: () => {
|
||||
Mock.mock(new RegExp('/api/message/list'), () => {
|
||||
return successResponseWrap(getMessageList())
|
||||
})
|
||||
|
||||
Mock.mock(new RegExp('/api/message/read'), (params: { body: string }) => {
|
||||
const { ids } = JSON.parse(params.body)
|
||||
haveReadIds.push(...(ids || []))
|
||||
return successResponseWrap(true)
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -1,100 +0,0 @@
|
||||
import Mock from 'mockjs'
|
||||
import setupMock, { successResponseWrap, failResponseWrap } from '@/utils/setup-mock'
|
||||
|
||||
import { MockParams } from '@/types/mock'
|
||||
import { isLogin } from '@/utils/auth'
|
||||
|
||||
setupMock({
|
||||
setup() {
|
||||
// Mock.XHR.prototype.withCredentials = true;
|
||||
// 用户信息
|
||||
Mock.mock(new RegExp('/api/user/info'), () => {
|
||||
if (isLogin()) {
|
||||
const role = window.localStorage.getItem('userRole') || 'admin'
|
||||
return successResponseWrap({
|
||||
name: '王立群',
|
||||
avatar: '//lf1-xgcdn-tos.pstatp.com/obj/vcloud/vadmin/start.8e0e4855ee346a46ccff8ff3e24db27b.png',
|
||||
email: 'wangliqun@email.com',
|
||||
job: 'frontend',
|
||||
jobName: '前端艺术家',
|
||||
organization: 'Frontend',
|
||||
organizationName: '前端',
|
||||
location: 'beijing',
|
||||
locationName: '北京',
|
||||
introduction: '人潇洒,性温存',
|
||||
personalWebsite: 'https://www.arco.design',
|
||||
phone: '150****0000',
|
||||
registrationDate: '2013-05-10 12:10:00',
|
||||
accountId: '15012312300',
|
||||
certification: 1,
|
||||
role,
|
||||
})
|
||||
}
|
||||
return failResponseWrap(null, '未登录', 50008)
|
||||
})
|
||||
|
||||
// 登录
|
||||
Mock.mock(new RegExp('/api/user/login'), (params: MockParams) => {
|
||||
const { username, password } = JSON.parse(params.body)
|
||||
if (!username) {
|
||||
return failResponseWrap(null, '用户名不能为空', 50000)
|
||||
}
|
||||
if (!password) {
|
||||
return failResponseWrap(null, '密码不能为空', 50000)
|
||||
}
|
||||
if (username === 'admin' && password === 'admin') {
|
||||
window.localStorage.setItem('userRole', 'admin')
|
||||
return successResponseWrap({
|
||||
token: '12345',
|
||||
})
|
||||
}
|
||||
if (username === 'user' && password === 'user') {
|
||||
window.localStorage.setItem('userRole', 'user')
|
||||
return successResponseWrap({
|
||||
token: '54321',
|
||||
})
|
||||
}
|
||||
return failResponseWrap(null, '账号或者密码错误', 50000)
|
||||
})
|
||||
|
||||
// 登出
|
||||
Mock.mock(new RegExp('/api/user/logout'), () => {
|
||||
return successResponseWrap(null)
|
||||
})
|
||||
|
||||
// 用户的服务端菜单
|
||||
Mock.mock(new RegExp('/api/user/menu'), () => {
|
||||
const menuList = [
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'dashboard',
|
||||
meta: {
|
||||
locale: 'menu.server.dashboard',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-dashboard',
|
||||
order: 1,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'workplace',
|
||||
name: 'Workplace',
|
||||
meta: {
|
||||
locale: 'menu.server.workplace',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'https://arco.design',
|
||||
name: 'arcoWebsite',
|
||||
meta: {
|
||||
locale: 'menu.arcoWebsite',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
return successResponseWrap(menuList)
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -1,68 +0,0 @@
|
||||
import Mock from 'mockjs'
|
||||
import setupMock, { successResponseWrap } from '@/utils/setup-mock'
|
||||
|
||||
const userListMock = Mock.mock({
|
||||
'data|52': [
|
||||
{
|
||||
'index|+1': 1,
|
||||
'name': '@cname',
|
||||
'status': '@integer(0,1)',
|
||||
'updateDate': '@datetime(yyyy-MM-dd)',
|
||||
'cellphone': '18782947123',
|
||||
'role': "@pick(['admin','user'])",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// 取get的查询参数,传入url解析出东西
|
||||
const getParams = (url: any) => {
|
||||
const urlp = url.split('?')[1]
|
||||
// 解析参数
|
||||
const queryStr = new URLSearchParams(urlp)
|
||||
const current: any = queryStr.get('current')
|
||||
const pageSize: any = queryStr.get('pageSize')
|
||||
return [current, pageSize]
|
||||
}
|
||||
|
||||
setupMock({
|
||||
setup() {
|
||||
Mock.mock(new RegExp('/api/user/list'), (params: any) => {
|
||||
const [current, pageSize] = getParams(params.url)
|
||||
const fyUerList = userListMock.data.slice((current - 1) * pageSize, current * pageSize)
|
||||
return successResponseWrap({ data: [...fyUerList], total: userListMock.data.length })
|
||||
})
|
||||
Mock.mock(new RegExp('/api/user/delete'), (payload) => {
|
||||
// 在mock中删除
|
||||
const id = payload.body
|
||||
const index = userListMock.data.findIndex((item: any) => item.index === id)
|
||||
userListMock.data.splice(index, 1)
|
||||
return successResponseWrap({ data: [], msg: '成功' })
|
||||
})
|
||||
Mock.mock(new RegExp('/api/user/all'), (payload) => {
|
||||
const option = JSON.parse(payload.body)
|
||||
// 根据name查询
|
||||
let newUserList = userListMock.data.filter((item: any) => item.name.includes(option.name))
|
||||
if (option.status) {
|
||||
newUserList = newUserList.filter((item: any) => item.status === option.status)
|
||||
}
|
||||
newUserList = newUserList.filter((item: any) => item.cellphone.includes(option.cellphone))
|
||||
newUserList = newUserList.filter((item: any) => {
|
||||
const updateDate1 = option.updateDate[0]
|
||||
const updateDate2 = option.updateDate[1]
|
||||
if (item.updateDate < updateDate1) {
|
||||
return false
|
||||
}
|
||||
if (item.updateDate > updateDate2) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
newUserList = newUserList.filter((item: any) => item.role.includes(option.role))
|
||||
const fyUerList = newUserList.slice(
|
||||
(option.current - 1) * option.pageSize,
|
||||
option.current * option.pageSize
|
||||
)
|
||||
return successResponseWrap({ data: [...fyUerList], total: newUserList.length })
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -1,16 +0,0 @@
|
||||
import { appRoutes, appExternalRoutes } from '../routes'
|
||||
|
||||
const mixinRoutes = [...appRoutes, ...appExternalRoutes]
|
||||
|
||||
const appClientMenus = mixinRoutes.map((el) => {
|
||||
const { name, path, meta, redirect, children } = el
|
||||
return {
|
||||
name,
|
||||
path,
|
||||
meta,
|
||||
redirect,
|
||||
children,
|
||||
}
|
||||
})
|
||||
|
||||
export default appClientMenus
|
||||
@@ -1,18 +0,0 @@
|
||||
export const WHITE_LIST = [
|
||||
{ name: 'notFound', children: [] },
|
||||
{ name: 'login', children: [] },
|
||||
]
|
||||
|
||||
export const NOT_FOUND = {
|
||||
name: 'notFound',
|
||||
}
|
||||
|
||||
export const REDIRECT_ROUTE_NAME = 'Redirect'
|
||||
|
||||
export const DEFAULT_ROUTE_NAME = 'Workplace'
|
||||
|
||||
export const DEFAULT_ROUTE = {
|
||||
title: 'menu.dashboard.workplace',
|
||||
name: DEFAULT_ROUTE_NAME,
|
||||
fullPath: '/dashboard/workplace',
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { Router } from 'vue-router'
|
||||
import { setRouteEmitter } from '@/utils/route-listener'
|
||||
import setupUserLoginInfoGuard from './userLoginInfo'
|
||||
import setupPermissionGuard from './permission'
|
||||
|
||||
function setupPageGuard(router: Router) {
|
||||
router.beforeEach(async (to) => {
|
||||
// emit route change
|
||||
setRouteEmitter(to)
|
||||
})
|
||||
}
|
||||
|
||||
export default function createRouteGuard(router: Router) {
|
||||
setupPageGuard(router)
|
||||
setupUserLoginInfoGuard(router)
|
||||
setupPermissionGuard(router)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import type { Router, RouteRecordNormalized } from 'vue-router'
|
||||
import NProgress from 'nprogress' // progress bar
|
||||
|
||||
import usePermission from '@/hooks/permission'
|
||||
import { useUserStore, useAppStore } from '@/store'
|
||||
import { appRoutes } from '../routes'
|
||||
import { WHITE_LIST, NOT_FOUND } from '../constants'
|
||||
|
||||
export default function setupPermissionGuard(router: Router) {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const appStore = useAppStore()
|
||||
const userStore = useUserStore()
|
||||
const Permission = usePermission()
|
||||
const permissionsAllow = Permission.accessRouter(to)
|
||||
if (appStore.menuFromServer) {
|
||||
// 针对来自服务端的菜单配置进行处理
|
||||
// Handle routing configuration from the server
|
||||
|
||||
// 根据需要自行完善来源于服务端的菜单配置的permission逻辑
|
||||
// Refine the permission logic from the server's menu configuration as needed
|
||||
if (!appStore.appAsyncMenus.length && !WHITE_LIST.find((el) => el.name === to.name)) {
|
||||
await appStore.fetchServerMenuConfig()
|
||||
}
|
||||
const serverMenuConfig = [...appStore.appAsyncMenus, ...WHITE_LIST]
|
||||
|
||||
let exist = false
|
||||
while (serverMenuConfig.length && !exist) {
|
||||
const element = serverMenuConfig.shift()
|
||||
if (element?.name === to.name) exist = true
|
||||
|
||||
if (element?.children) {
|
||||
serverMenuConfig.push(...(element.children as unknown as RouteRecordNormalized[]))
|
||||
}
|
||||
}
|
||||
if (exist && permissionsAllow) {
|
||||
next()
|
||||
} else next(NOT_FOUND)
|
||||
} else {
|
||||
// eslint-disable-next-line no-lonely-if
|
||||
if (permissionsAllow) next()
|
||||
else {
|
||||
const destination = Permission.findFirstPermissionRoute(appRoutes, userStore.role) || NOT_FOUND
|
||||
next(destination)
|
||||
}
|
||||
}
|
||||
NProgress.done()
|
||||
})
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { Router, LocationQueryRaw } from 'vue-router'
|
||||
import NProgress from 'nprogress' // progress bar
|
||||
|
||||
import { useUserStore } from '@/store'
|
||||
import { isLogin } from '@/utils/auth'
|
||||
|
||||
export default function setupUserLoginInfoGuard(router: Router) {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
NProgress.start()
|
||||
const userStore = useUserStore()
|
||||
if (isLogin()) {
|
||||
if (userStore.role) {
|
||||
next()
|
||||
} else {
|
||||
try {
|
||||
await userStore.info()
|
||||
next()
|
||||
} catch (error) {
|
||||
await userStore.logout()
|
||||
next({
|
||||
name: 'login',
|
||||
query: {
|
||||
redirect: to.name,
|
||||
...to.query,
|
||||
} as LocationQueryRaw,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (to.name === 'login') {
|
||||
next()
|
||||
return
|
||||
}
|
||||
next({
|
||||
name: 'login',
|
||||
query: {
|
||||
redirect: to.name,
|
||||
...to.query,
|
||||
} as LocationQueryRaw,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import NProgress from 'nprogress' // progress bar
|
||||
import 'nprogress/nprogress.css'
|
||||
|
||||
import { appRoutes } from './routes'
|
||||
import { REDIRECT_MAIN, NOT_FOUND_ROUTE } from './routes/base'
|
||||
import createRouteGuard from './guard'
|
||||
|
||||
NProgress.configure({ showSpinner: false }) // NProgress Configuration
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
redirect: 'login',
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('@/views/login/index.vue'),
|
||||
meta: {
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
...appRoutes,
|
||||
REDIRECT_MAIN,
|
||||
NOT_FOUND_ROUTE,
|
||||
],
|
||||
scrollBehavior() {
|
||||
return { top: 0 }
|
||||
},
|
||||
})
|
||||
|
||||
createRouteGuard(router)
|
||||
|
||||
export default router
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { REDIRECT_ROUTE_NAME } from '@/router/constants'
|
||||
|
||||
export const DEFAULT_LAYOUT = () => import('@/layout/default-layout.vue')
|
||||
|
||||
export const REDIRECT_MAIN: RouteRecordRaw = {
|
||||
path: '/redirect',
|
||||
name: 'redirectWrapper',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
hideInMenu: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/redirect/:path',
|
||||
name: REDIRECT_ROUTE_NAME,
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const NOT_FOUND_ROUTE: RouteRecordRaw = {
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'notFound',
|
||||
component: () => import('@/views/not-found/index.vue'),
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { RouteRecordNormalized } from 'vue-router'
|
||||
|
||||
const modules = import.meta.glob('./modules/*.ts', { eager: true })
|
||||
const externalModules = import.meta.glob('./externalModules/*.ts', {
|
||||
eager: true,
|
||||
})
|
||||
|
||||
function formatModules(_modules: any, result: RouteRecordNormalized[]) {
|
||||
Object.keys(_modules).forEach((key) => {
|
||||
const defaultModule = _modules[key].default
|
||||
if (!defaultModule) return
|
||||
const moduleList = Array.isArray(defaultModule) ? [...defaultModule] : [defaultModule]
|
||||
result.push(...moduleList)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
export const appRoutes: RouteRecordNormalized[] = formatModules(modules, [])
|
||||
|
||||
export const appExternalRoutes: RouteRecordNormalized[] = formatModules(externalModules, [])
|
||||
@@ -1,38 +0,0 @@
|
||||
import { DEFAULT_LAYOUT } from '../base'
|
||||
import { AppRouteRecordRaw } from '../types'
|
||||
|
||||
const DASHBOARD: AppRouteRecordRaw = {
|
||||
path: '/dashboard',
|
||||
name: 'dashboard',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: 'menu.dashboard',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-dashboard',
|
||||
order: 0,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'workplace',
|
||||
name: 'Workplace',
|
||||
component: () => import('@/views/dashboard/workplace/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.dashboard.workplace',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'monitor',
|
||||
name: 'monitor',
|
||||
component: () => import('@/views/dashboard/monitor/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.dashboard.monitor',
|
||||
requiresAuth: true,
|
||||
roles: ['admin'],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default DASHBOARD
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user