Compare commits
2 Commits
master
...
feature/cs
| Author | SHA1 | Date |
|---|---|---|
|
|
15b85dd9c1 | |
|
|
5c7b605436 |
|
|
@ -4,3 +4,4 @@ public
|
|||
*.config.js
|
||||
*.config.ts
|
||||
start-debug.js
|
||||
src/assets/iconfont
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[less]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
"editor.defaultFormatter": "vscode.css-language-features"
|
||||
},
|
||||
"files.associations": {
|
||||
"*.vue": "vue"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: {
|
||||
chrome: '80',
|
||||
firefox: '75',
|
||||
safari: '14',
|
||||
edge: '80',
|
||||
},
|
||||
useBuiltIns: 'usage',
|
||||
corejs: 3,
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
{
|
||||
"name": "cpms_web_admin_pc",
|
||||
"version": "1.0.0",
|
||||
"name": "cpms_operation_platform",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cpms_web_admin_pc",
|
||||
"version": "1.0.0",
|
||||
"name": "cpms_operation_platform",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"@yp-component/root": "^0.0.46",
|
||||
"ant-design-vue": "^4.0.0",
|
||||
"express": "^5.2.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
|
|
@ -21,20 +22,21 @@
|
|||
"@commitlint/config-conventional": "^20.4.3",
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/qs": "^6.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-vue": "^4.5.0",
|
||||
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
||||
"eslint": "^8.0.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-vue": "^9.0.0",
|
||||
"eslint-plugin-vue": "^9.33.0",
|
||||
"husky": "^9.1.7",
|
||||
"less": "^4.6.4",
|
||||
"lint-staged": "^15.0.0",
|
||||
"prettier": "^3.0.0",
|
||||
"typescript": "^5.3.0",
|
||||
"vite": "^5.0.0",
|
||||
"vue-eslint-parser": "^10.4.1",
|
||||
"vue-tsc": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -984,6 +986,13 @@
|
|||
"url": "https://ko-fi.com/dangreen"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/esrecurse": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "http://npm.nps.yunvip123.cn/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
||||
"integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"dev": true,
|
||||
|
|
@ -1014,6 +1023,8 @@
|
|||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "http://npm.nps.yunvip123.cn/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
|
||||
"integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -1048,6 +1059,8 @@
|
|||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "http://npm.nps.yunvip123.cn/@typescript-eslint/parser/-/parser-6.21.0.tgz",
|
||||
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
|
|
@ -1427,6 +1440,15 @@
|
|||
"version": "3.5.39",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@yp-component/root": {
|
||||
"version": "0.0.46",
|
||||
"resolved": "http://npm.nps.yunvip123.cn/@yp-component/root/-/root-0.0.46.tgz",
|
||||
"integrity": "sha512-umgaFa2x/gEtlvXsrqbu9lgcUU/j8K8Jj4p1n45vJK6c5ysBMQMj6sYWT0Jr8YO65ofh9BSMPl17n8jlknY5xA==",
|
||||
"dependencies": {
|
||||
"cropperjs": "^1.6.2",
|
||||
"vue": "^3.5.25"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
|
|
@ -2034,6 +2056,12 @@
|
|||
"typescript": ">=5"
|
||||
}
|
||||
},
|
||||
"node_modules/cropperjs": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "http://npm.nps.yunvip123.cn/cropperjs/-/cropperjs-1.6.2.tgz",
|
||||
"integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"dev": true,
|
||||
|
|
@ -2699,6 +2727,9 @@
|
|||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "8.57.1",
|
||||
"resolved": "http://npm.nps.yunvip123.cn/eslint/-/eslint-8.57.1.tgz",
|
||||
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
|
||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -2793,6 +2824,8 @@
|
|||
},
|
||||
"node_modules/eslint-plugin-vue": {
|
||||
"version": "9.33.0",
|
||||
"resolved": "http://npm.nps.yunvip123.cn/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz",
|
||||
"integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -2812,6 +2845,31 @@
|
|||
"eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-vue/node_modules/vue-eslint-parser": {
|
||||
"version": "9.4.3",
|
||||
"resolved": "http://npm.nps.yunvip123.cn/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",
|
||||
"integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"eslint-scope": "^7.1.1",
|
||||
"eslint-visitor-keys": "^3.3.0",
|
||||
"espree": "^9.3.1",
|
||||
"esquery": "^1.4.0",
|
||||
"lodash": "^4.17.21",
|
||||
"semver": "^7.3.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mysticatea"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "7.2.2",
|
||||
"dev": true,
|
||||
|
|
@ -5748,26 +5806,77 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vue-eslint-parser": {
|
||||
"version": "9.4.3",
|
||||
"version": "10.4.1",
|
||||
"resolved": "http://npm.nps.yunvip123.cn/vue-eslint-parser/-/vue-eslint-parser-10.4.1.tgz",
|
||||
"integrity": "sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"eslint-scope": "^7.1.1",
|
||||
"eslint-visitor-keys": "^3.3.0",
|
||||
"espree": "^9.3.1",
|
||||
"esquery": "^1.4.0",
|
||||
"lodash": "^4.17.21",
|
||||
"semver": "^7.3.6"
|
||||
"debug": "^4.4.0",
|
||||
"eslint-scope": "^8.2.0 || ^9.0.0",
|
||||
"eslint-visitor-keys": "^4.2.0 || ^5.0.0",
|
||||
"espree": "^10.3.0 || ^11.0.0",
|
||||
"esquery": "^1.6.0",
|
||||
"semver": "^7.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || >=16.0.0"
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mysticatea"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=6.0.0"
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-eslint-parser/node_modules/eslint-scope": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "http://npm.nps.yunvip123.cn/eslint-scope/-/eslint-scope-9.1.2.tgz",
|
||||
"integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@types/esrecurse": "^4.3.1",
|
||||
"@types/estree": "^1.0.8",
|
||||
"esrecurse": "^4.3.0",
|
||||
"estraverse": "^5.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "http://npm.nps.yunvip123.cn/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-eslint-parser/node_modules/espree": {
|
||||
"version": "11.2.0",
|
||||
"resolved": "http://npm.nps.yunvip123.cn/espree/-/espree-11.2.0.tgz",
|
||||
"integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"acorn": "^8.16.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
"eslint-visitor-keys": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
|
|
|
|||
|
|
@ -33,20 +33,21 @@
|
|||
"@commitlint/config-conventional": "^20.4.3",
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/qs": "^6.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-vue": "^4.5.0",
|
||||
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
||||
"eslint": "^8.0.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-vue": "^9.0.0",
|
||||
"eslint-plugin-vue": "^9.33.0",
|
||||
"husky": "^9.1.7",
|
||||
"less": "^4.6.4",
|
||||
"lint-staged": "^15.0.0",
|
||||
"prettier": "^3.0.0",
|
||||
"typescript": "^5.3.0",
|
||||
"vite": "^5.0.0",
|
||||
"vue-eslint-parser": "^10.4.1",
|
||||
"vue-tsc": "^2.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
|
|
|||
12
src/App.tsx
12
src/App.tsx
|
|
@ -12,7 +12,17 @@ export default defineComponent({
|
|||
setup() {
|
||||
const theme = {
|
||||
token: {
|
||||
colorPrimary: '#1677ff',
|
||||
colorPrimary: '#1890ff',
|
||||
colorSuccess: '#52c41a',
|
||||
colorWarning: '#faad14',
|
||||
colorError: '#ff4d4f',
|
||||
colorInfo: '#1890ff',
|
||||
colorBgBase: '#ffffff',
|
||||
colorBgContainer: '#ffffff',
|
||||
colorBgElevated: '#ffffff',
|
||||
colorBgLayout: '#f0f2f5',
|
||||
colorTextBase: '#262626',
|
||||
borderRadius: 6,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,545 @@
|
|||
/* Logo 字体 */
|
||||
@font-face {
|
||||
font-family: 'iconfont logo';
|
||||
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
|
||||
src:
|
||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix')
|
||||
format('embedded-opentype'),
|
||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
|
||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
|
||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont')
|
||||
format('svg');
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: 'iconfont logo';
|
||||
font-size: 160px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* tabs */
|
||||
.nav-tabs {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-more {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 42px;
|
||||
line-height: 42px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#tabs {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
#tabs li {
|
||||
cursor: pointer;
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
border-bottom: 2px solid transparent;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-bottom: -1px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#tabs .active {
|
||||
border-bottom-color: #f00;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.tab-container .content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 页面布局 */
|
||||
.main {
|
||||
padding: 30px 100px;
|
||||
width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.main .logo {
|
||||
color: #333;
|
||||
text-align: left;
|
||||
margin-bottom: 30px;
|
||||
line-height: 1;
|
||||
height: 110px;
|
||||
margin-top: -50px;
|
||||
overflow: hidden;
|
||||
*zoom: 1;
|
||||
}
|
||||
|
||||
.main .logo a {
|
||||
font-size: 160px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.helps {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.helps pre {
|
||||
padding: 20px;
|
||||
margin: 10px 0;
|
||||
border: solid 1px #e7e1cd;
|
||||
background-color: #fffdef;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.icon_lists {
|
||||
width: 100% !important;
|
||||
overflow: hidden;
|
||||
*zoom: 1;
|
||||
}
|
||||
|
||||
.icon_lists li {
|
||||
width: 100px;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 20px;
|
||||
text-align: center;
|
||||
list-style: none !important;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.icon_lists li .code-name {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.icon_lists .icon {
|
||||
display: block;
|
||||
height: 100px;
|
||||
line-height: 100px;
|
||||
font-size: 42px;
|
||||
margin: 10px auto;
|
||||
color: #333;
|
||||
-webkit-transition:
|
||||
font-size 0.25s linear,
|
||||
width 0.25s linear;
|
||||
-moz-transition:
|
||||
font-size 0.25s linear,
|
||||
width 0.25s linear;
|
||||
transition:
|
||||
font-size 0.25s linear,
|
||||
width 0.25s linear;
|
||||
}
|
||||
|
||||
.icon_lists .icon:hover {
|
||||
font-size: 100px;
|
||||
}
|
||||
|
||||
.icon_lists .svg-icon {
|
||||
/* 通过设置 font-size 来改变图标大小 */
|
||||
width: 1em;
|
||||
/* 图标和文字相邻时,垂直对齐 */
|
||||
vertical-align: -0.15em;
|
||||
/* 通过设置 color 来改变 SVG 的颜色/fill */
|
||||
fill: currentColor;
|
||||
/* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
|
||||
normalize.css 中也包含这行 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.icon_lists li .name,
|
||||
.icon_lists li .code-name {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* markdown 样式 */
|
||||
.markdown {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.markdown img {
|
||||
vertical-align: middle;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.markdown h1 {
|
||||
color: #404040;
|
||||
font-weight: 500;
|
||||
line-height: 40px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.markdown h2,
|
||||
.markdown h3,
|
||||
.markdown h4,
|
||||
.markdown h5,
|
||||
.markdown h6 {
|
||||
color: #404040;
|
||||
margin: 1.6em 0 0.6em 0;
|
||||
font-weight: 500;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.markdown h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.markdown h2 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.markdown h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.markdown h4 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.markdown h5 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.markdown h6 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.markdown hr {
|
||||
height: 1px;
|
||||
border: 0;
|
||||
background: #e9e9e9;
|
||||
margin: 16px 0;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.markdown p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown > p,
|
||||
.markdown > blockquote,
|
||||
.markdown > .highlight,
|
||||
.markdown > ol,
|
||||
.markdown > ul {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.markdown ul > li {
|
||||
list-style: circle;
|
||||
}
|
||||
|
||||
.markdown > ul li,
|
||||
.markdown blockquote ul > li {
|
||||
margin-left: 20px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.markdown > ul li p,
|
||||
.markdown > ol li p {
|
||||
margin: 0.6em 0;
|
||||
}
|
||||
|
||||
.markdown ol > li {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
.markdown > ol li,
|
||||
.markdown blockquote ol > li {
|
||||
margin-left: 20px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.markdown code {
|
||||
margin: 0 3px;
|
||||
padding: 0 5px;
|
||||
background: #eee;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.markdown strong,
|
||||
.markdown b {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown > table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0px;
|
||||
empty-cells: show;
|
||||
border: 1px solid #e9e9e9;
|
||||
width: 95%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.markdown > table th {
|
||||
white-space: nowrap;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown > table th,
|
||||
.markdown > table td {
|
||||
border: 1px solid #e9e9e9;
|
||||
padding: 8px 16px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown > table th {
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.markdown blockquote {
|
||||
font-size: 90%;
|
||||
color: #999;
|
||||
border-left: 4px solid #e9e9e9;
|
||||
padding-left: 0.8em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown blockquote p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.markdown .anchor {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.markdown .waiting {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.markdown h1:hover .anchor,
|
||||
.markdown h2:hover .anchor,
|
||||
.markdown h3:hover .anchor,
|
||||
.markdown h4:hover .anchor,
|
||||
.markdown h5:hover .anchor,
|
||||
.markdown h6:hover .anchor {
|
||||
opacity: 1;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.markdown > br,
|
||||
.markdown > p > br {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
background: white;
|
||||
padding: 0.5em;
|
||||
color: #333333;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-meta {
|
||||
color: #969896;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-strong,
|
||||
.hljs-emphasis,
|
||||
.hljs-quote {
|
||||
color: #df5000;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-type {
|
||||
color: #a71d5d;
|
||||
}
|
||||
|
||||
.hljs-literal,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-attribute {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-section,
|
||||
.hljs-name {
|
||||
color: #63a35c;
|
||||
}
|
||||
|
||||
.hljs-tag {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-attr,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo {
|
||||
color: #795da3;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #55a532;
|
||||
background-color: #eaffea;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #bd2c00;
|
||||
background-color: #ffecec;
|
||||
}
|
||||
|
||||
.hljs-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 代码高亮 */
|
||||
/* PrismJS 1.15.0
|
||||
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
|
||||
/**
|
||||
* prism.js default theme for JavaScript, CSS and HTML
|
||||
* Based on dabblet (http://dabblet.com)
|
||||
* @author Lea Verou
|
||||
*/
|
||||
code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
color: black;
|
||||
background: none;
|
||||
text-shadow: 0 1px white;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
pre[class*='language-']::-moz-selection,
|
||||
pre[class*='language-'] ::-moz-selection,
|
||||
code[class*='language-']::-moz-selection,
|
||||
code[class*='language-'] ::-moz-selection {
|
||||
text-shadow: none;
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
pre[class*='language-']::selection,
|
||||
pre[class*='language-'] ::selection,
|
||||
code[class*='language-']::selection,
|
||||
code[class*='language-'] ::selection {
|
||||
text-shadow: none;
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
@media print {
|
||||
code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*='language-'] {
|
||||
padding: 1em;
|
||||
margin: 0.5em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
background: #f5f2f0;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*='language-'] {
|
||||
padding: 0.1em;
|
||||
border-radius: 0.3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: slategray;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.namespace {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #905;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #690;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #9a6e3a;
|
||||
background: hsla(0, 0%, 100%, 0.5);
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: #07a;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #dd4a68;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important,
|
||||
.token.variable {
|
||||
color: #e90;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,380 @@
|
|||
{
|
||||
"id": "4533260",
|
||||
"name": "改版-六个羽友",
|
||||
"font_family": "iconfont",
|
||||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "43518948",
|
||||
"name": "场馆",
|
||||
"font_class": "changguan1",
|
||||
"unicode": "e87b",
|
||||
"unicode_decimal": 59515
|
||||
},
|
||||
{
|
||||
"icon_id": "31394058",
|
||||
"name": "灯-点亮",
|
||||
"font_class": "deng-dianliang",
|
||||
"unicode": "e7b9",
|
||||
"unicode_decimal": 59321
|
||||
},
|
||||
{
|
||||
"icon_id": "31394059",
|
||||
"name": "灯-关闭",
|
||||
"font_class": "deng-guanbi",
|
||||
"unicode": "e7ba",
|
||||
"unicode_decimal": 59322
|
||||
},
|
||||
{
|
||||
"icon_id": "43469992",
|
||||
"name": "刷新",
|
||||
"font_class": "shuaxin",
|
||||
"unicode": "e878",
|
||||
"unicode_decimal": 59512
|
||||
},
|
||||
{
|
||||
"icon_id": "43469991",
|
||||
"name": "关闭",
|
||||
"font_class": "guanbi1",
|
||||
"unicode": "e879",
|
||||
"unicode_decimal": 59513
|
||||
},
|
||||
{
|
||||
"icon_id": "39274830",
|
||||
"name": "添加",
|
||||
"font_class": "tianjia1",
|
||||
"unicode": "e81f",
|
||||
"unicode_decimal": 59423
|
||||
},
|
||||
{
|
||||
"icon_id": "41007799",
|
||||
"name": "签到码",
|
||||
"font_class": "qiandaoma",
|
||||
"unicode": "e857",
|
||||
"unicode_decimal": 59479
|
||||
},
|
||||
{
|
||||
"icon_id": "40996457",
|
||||
"name": "日历",
|
||||
"font_class": "rili1",
|
||||
"unicode": "e856",
|
||||
"unicode_decimal": 59478
|
||||
},
|
||||
{
|
||||
"icon_id": "40820828",
|
||||
"name": "分组title",
|
||||
"font_class": "fenzutitle",
|
||||
"unicode": "e84c",
|
||||
"unicode_decimal": 59468
|
||||
},
|
||||
{
|
||||
"icon_id": "40820822",
|
||||
"name": "成功",
|
||||
"font_class": "chenggong",
|
||||
"unicode": "e84d",
|
||||
"unicode_decimal": 59469
|
||||
},
|
||||
{
|
||||
"icon_id": "40820826",
|
||||
"name": "存草稿",
|
||||
"font_class": "cuncaogao",
|
||||
"unicode": "e84e",
|
||||
"unicode_decimal": 59470
|
||||
},
|
||||
{
|
||||
"icon_id": "40820824",
|
||||
"name": "图片",
|
||||
"font_class": "tupian",
|
||||
"unicode": "e84f",
|
||||
"unicode_decimal": 59471
|
||||
},
|
||||
{
|
||||
"icon_id": "40820823",
|
||||
"name": "微信支付",
|
||||
"font_class": "weixinzhifu1",
|
||||
"unicode": "e850",
|
||||
"unicode_decimal": 59472
|
||||
},
|
||||
{
|
||||
"icon_id": "40820821",
|
||||
"name": "删除",
|
||||
"font_class": "shanchu",
|
||||
"unicode": "e851",
|
||||
"unicode_decimal": 59473
|
||||
},
|
||||
{
|
||||
"icon_id": "40820818",
|
||||
"name": "钱包",
|
||||
"font_class": "qianbao",
|
||||
"unicode": "e852",
|
||||
"unicode_decimal": 59474
|
||||
},
|
||||
{
|
||||
"icon_id": "40820816",
|
||||
"name": "男",
|
||||
"font_class": "nan",
|
||||
"unicode": "e853",
|
||||
"unicode_decimal": 59475
|
||||
},
|
||||
{
|
||||
"icon_id": "40820819",
|
||||
"name": "金币",
|
||||
"font_class": "jinbi",
|
||||
"unicode": "e854",
|
||||
"unicode_decimal": 59476
|
||||
},
|
||||
{
|
||||
"icon_id": "40820817",
|
||||
"name": "女",
|
||||
"font_class": "nv",
|
||||
"unicode": "e855",
|
||||
"unicode_decimal": 59477
|
||||
},
|
||||
{
|
||||
"icon_id": "40820832",
|
||||
"name": "分享",
|
||||
"font_class": "fenxiang1",
|
||||
"unicode": "e847",
|
||||
"unicode_decimal": 59463
|
||||
},
|
||||
{
|
||||
"icon_id": "40820825",
|
||||
"name": "修改",
|
||||
"font_class": "xiugai",
|
||||
"unicode": "e848",
|
||||
"unicode_decimal": 59464
|
||||
},
|
||||
{
|
||||
"icon_id": "40820830",
|
||||
"name": "编辑",
|
||||
"font_class": "bianji",
|
||||
"unicode": "e849",
|
||||
"unicode_decimal": 59465
|
||||
},
|
||||
{
|
||||
"icon_id": "40820829",
|
||||
"name": "更多1",
|
||||
"font_class": "gengduo1",
|
||||
"unicode": "e84a",
|
||||
"unicode_decimal": 59466
|
||||
},
|
||||
{
|
||||
"icon_id": "40820820",
|
||||
"name": "上",
|
||||
"font_class": "shang",
|
||||
"unicode": "e84b",
|
||||
"unicode_decimal": 59467
|
||||
},
|
||||
{
|
||||
"icon_id": "40820837",
|
||||
"name": "添加",
|
||||
"font_class": "tianjia",
|
||||
"unicode": "e840",
|
||||
"unicode_decimal": 59456
|
||||
},
|
||||
{
|
||||
"icon_id": "40820836",
|
||||
"name": "更多",
|
||||
"font_class": "gengduo",
|
||||
"unicode": "e841",
|
||||
"unicode_decimal": 59457
|
||||
},
|
||||
{
|
||||
"icon_id": "40820835",
|
||||
"name": "电话",
|
||||
"font_class": "dianhua",
|
||||
"unicode": "e842",
|
||||
"unicode_decimal": 59458
|
||||
},
|
||||
{
|
||||
"icon_id": "40820827",
|
||||
"name": "撤销",
|
||||
"font_class": "chexiao",
|
||||
"unicode": "e843",
|
||||
"unicode_decimal": 59459
|
||||
},
|
||||
{
|
||||
"icon_id": "40820831",
|
||||
"name": "微信",
|
||||
"font_class": "weixin",
|
||||
"unicode": "e844",
|
||||
"unicode_decimal": 59460
|
||||
},
|
||||
{
|
||||
"icon_id": "40820834",
|
||||
"name": "分组",
|
||||
"font_class": "fenzu",
|
||||
"unicode": "e845",
|
||||
"unicode_decimal": 59461
|
||||
},
|
||||
{
|
||||
"icon_id": "40820833",
|
||||
"name": "点名",
|
||||
"font_class": "dianming",
|
||||
"unicode": "e846",
|
||||
"unicode_decimal": 59462
|
||||
},
|
||||
{
|
||||
"icon_id": "36759285",
|
||||
"name": "套餐",
|
||||
"font_class": "taocan",
|
||||
"unicode": "e7fc",
|
||||
"unicode_decimal": 59388
|
||||
},
|
||||
{
|
||||
"icon_id": "33120817",
|
||||
"name": "特价",
|
||||
"font_class": "tejia",
|
||||
"unicode": "e7da",
|
||||
"unicode_decimal": 59354
|
||||
},
|
||||
{
|
||||
"icon_id": "33120818",
|
||||
"name": "满减",
|
||||
"font_class": "manjian",
|
||||
"unicode": "e7db",
|
||||
"unicode_decimal": 59355
|
||||
},
|
||||
{
|
||||
"icon_id": "40351403",
|
||||
"name": "票",
|
||||
"font_class": "piao",
|
||||
"unicode": "e83f",
|
||||
"unicode_decimal": 59455
|
||||
},
|
||||
{
|
||||
"icon_id": "848275",
|
||||
"name": "勾",
|
||||
"font_class": "gou",
|
||||
"unicode": "e63d",
|
||||
"unicode_decimal": 58941
|
||||
},
|
||||
{
|
||||
"icon_id": "40248532",
|
||||
"name": "分享",
|
||||
"font_class": "fenxiang",
|
||||
"unicode": "e83e",
|
||||
"unicode_decimal": 59454
|
||||
},
|
||||
{
|
||||
"icon_id": "40216674",
|
||||
"name": "浏览量",
|
||||
"font_class": "liulanliang",
|
||||
"unicode": "e83c",
|
||||
"unicode_decimal": 59452
|
||||
},
|
||||
{
|
||||
"icon_id": "40216675",
|
||||
"name": "日历",
|
||||
"font_class": "rili",
|
||||
"unicode": "e83d",
|
||||
"unicode_decimal": 59453
|
||||
},
|
||||
{
|
||||
"icon_id": "40191312",
|
||||
"name": "左",
|
||||
"font_class": "zuo",
|
||||
"unicode": "e83b",
|
||||
"unicode_decimal": 59451
|
||||
},
|
||||
{
|
||||
"icon_id": "40191242",
|
||||
"name": "下",
|
||||
"font_class": "xia",
|
||||
"unicode": "e83a",
|
||||
"unicode_decimal": 59450
|
||||
},
|
||||
{
|
||||
"icon_id": "40191226",
|
||||
"name": "定位",
|
||||
"font_class": "dingwei",
|
||||
"unicode": "e82d",
|
||||
"unicode_decimal": 59437
|
||||
},
|
||||
{
|
||||
"icon_id": "40191227",
|
||||
"name": "关闭",
|
||||
"font_class": "guanbi",
|
||||
"unicode": "e82e",
|
||||
"unicode_decimal": 59438
|
||||
},
|
||||
{
|
||||
"icon_id": "40191228",
|
||||
"name": "收藏",
|
||||
"font_class": "shoucang",
|
||||
"unicode": "e82f",
|
||||
"unicode_decimal": 59439
|
||||
},
|
||||
{
|
||||
"icon_id": "40191229",
|
||||
"name": "时间",
|
||||
"font_class": "shijian",
|
||||
"unicode": "e830",
|
||||
"unicode_decimal": 59440
|
||||
},
|
||||
{
|
||||
"icon_id": "40191230",
|
||||
"name": "场馆",
|
||||
"font_class": "changguan",
|
||||
"unicode": "e831",
|
||||
"unicode_decimal": 59441
|
||||
},
|
||||
{
|
||||
"icon_id": "40191231",
|
||||
"name": "未选中",
|
||||
"font_class": "weixuanzhong",
|
||||
"unicode": "e832",
|
||||
"unicode_decimal": 59442
|
||||
},
|
||||
{
|
||||
"icon_id": "40191232",
|
||||
"name": "选择",
|
||||
"font_class": "xuanze",
|
||||
"unicode": "e833",
|
||||
"unicode_decimal": 59443
|
||||
},
|
||||
{
|
||||
"icon_id": "40191233",
|
||||
"name": "选择-fill",
|
||||
"font_class": "xuanze-fill",
|
||||
"unicode": "e834",
|
||||
"unicode_decimal": 59444
|
||||
},
|
||||
{
|
||||
"icon_id": "40191234",
|
||||
"name": "选中",
|
||||
"font_class": "xuanzhong",
|
||||
"unicode": "e835",
|
||||
"unicode_decimal": 59445
|
||||
},
|
||||
{
|
||||
"icon_id": "40191236",
|
||||
"name": "微信支付",
|
||||
"font_class": "weixinzhifu",
|
||||
"unicode": "e836",
|
||||
"unicode_decimal": 59446
|
||||
},
|
||||
{
|
||||
"icon_id": "40191237",
|
||||
"name": "右",
|
||||
"font_class": "you",
|
||||
"unicode": "e837",
|
||||
"unicode_decimal": 59447
|
||||
},
|
||||
{
|
||||
"icon_id": "40191238",
|
||||
"name": "联系我们",
|
||||
"font_class": "lianxiwomen",
|
||||
"unicode": "e838",
|
||||
"unicode_decimal": 59448
|
||||
},
|
||||
{
|
||||
"icon_id": "40191239",
|
||||
"name": "余额",
|
||||
"font_class": "yue",
|
||||
"unicode": "e839",
|
||||
"unicode_decimal": 59449
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 66 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -55,3 +55,13 @@ a {
|
|||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.match-ellipsis-single {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ant-layout .ant-layout-header {
|
||||
padding-inline: 15px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,46 +1,99 @@
|
|||
import { h } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { TrophyOutlined, WalletOutlined } from '@ant-design/icons-vue';
|
||||
import { routes } from '@/router/routes';
|
||||
import { generateMenu, type MenuItem } from '@/router/utils';
|
||||
import { useBreadcrumb } from './useBreadcrumb';
|
||||
|
||||
/**
|
||||
* 基础布局管理 Hook
|
||||
*
|
||||
* @description
|
||||
* 管理页面布局的核心逻辑。菜单、面包屑均由「路由表」派生,路由是唯一数据源:
|
||||
* - 侧边栏菜单:generateMenu 根据路由的 meta.title / meta.icon 生成;
|
||||
* - 菜单高亮 / 展开:根据当前路由匹配;
|
||||
* - 面包屑:见 useBreadcrumb,取自 route.matched。
|
||||
*/
|
||||
export function useBasicLayout() {
|
||||
// 路由实例,用于页面跳转
|
||||
const router = useRouter();
|
||||
// 当前路由信息
|
||||
const route = useRoute();
|
||||
// 获取面包屑数据
|
||||
const { breadcrumbItems } = useBreadcrumb();
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
key: '/events',
|
||||
icon: h(TrophyOutlined),
|
||||
label: '我的赛事',
|
||||
},
|
||||
// { TODO 本期先做隐藏
|
||||
// key: '/wallet',
|
||||
// icon: h(WalletOutlined),
|
||||
// label: '钱包管理',
|
||||
// },
|
||||
];
|
||||
/**
|
||||
* 侧边栏菜单配置
|
||||
* 由主布局("/")的子路由生成,不再手写维护
|
||||
*/
|
||||
const layoutChildren = routes.find((r) => r.path === '/' && r.children?.length)?.children ?? [];
|
||||
const menuItems = generateMenu(layoutChildren);
|
||||
|
||||
/**
|
||||
* 收集所有叶子菜单(可点击、可高亮的具体菜单项)的 key
|
||||
* 父级 submenu 不参与 selected 高亮,只有叶子项才精确匹配当前路由
|
||||
*/
|
||||
const leafMenuKeys = new Set<string>();
|
||||
const collectLeafKeys = (items: MenuItem[]) => {
|
||||
items.forEach((item) => {
|
||||
if (item.children?.length) {
|
||||
collectLeafKeys(item.children);
|
||||
} else {
|
||||
leafMenuKeys.add(item.key);
|
||||
}
|
||||
});
|
||||
};
|
||||
collectLeafKeys(menuItems);
|
||||
|
||||
/**
|
||||
* 获取当前激活的菜单 key(侧边栏高亮项)
|
||||
*
|
||||
* 优先级:
|
||||
* 1. 当前路径命中某个叶子菜单 -> 高亮该菜单项
|
||||
* 2. 路由 meta.activeMenu 配置(详情页等路径不等于菜单 key 的场景)
|
||||
* 3. 默认返回当前路由路径
|
||||
*/
|
||||
const getActiveMenuKey = () => {
|
||||
const matchedRoutes = route.matched;
|
||||
for (const r of matchedRoutes) {
|
||||
if (leafMenuKeys.has(route.path)) {
|
||||
return route.path;
|
||||
}
|
||||
for (const r of route.matched) {
|
||||
if (r.meta?.activeMenu) {
|
||||
return r.meta.activeMenu as string;
|
||||
}
|
||||
}
|
||||
if (route.path.startsWith('/events')) return '/events';
|
||||
if (route.path.startsWith('/wallet')) return '/wallet';
|
||||
return route.path;
|
||||
};
|
||||
|
||||
const handleMenuClick = (info: any) => {
|
||||
/**
|
||||
* 获取需要展开的子菜单 keys
|
||||
* 根据当前路径匹配所有「含子菜单且为当前路径祖先」的父级菜单
|
||||
*/
|
||||
const getOpenKeys = () => {
|
||||
return menuItems
|
||||
.filter(
|
||||
(item) =>
|
||||
item.children?.length &&
|
||||
(route.path === item.key || route.path.startsWith(`${item.key}/`)),
|
||||
)
|
||||
.map((item) => item.key);
|
||||
};
|
||||
|
||||
/**
|
||||
* 菜单点击处理:跳转到对应路由
|
||||
*/
|
||||
const handleMenuClick = (info: { key: string | number }) => {
|
||||
router.push(String(info.key));
|
||||
};
|
||||
|
||||
return {
|
||||
// 面包屑数据
|
||||
breadcrumbItems,
|
||||
// 菜单配置(由路由生成)
|
||||
menuItems,
|
||||
// 获取激活菜单 key
|
||||
getActiveMenuKey,
|
||||
// 获取展开菜单 keys
|
||||
getOpenKeys,
|
||||
// 菜单点击处理
|
||||
handleMenuClick,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,39 @@
|
|||
import { computed, h } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
/**
|
||||
* 面包屑管理 Hook
|
||||
*
|
||||
* @description
|
||||
* 面包屑完全由当前路由派生:遍历 route.matched(命中的路由链,从父到子),
|
||||
* 取每一层 meta.title 作为面包屑标题。层级关系写在路由表里(分组路由 + 子路由),
|
||||
* 不再维护额外的菜单/映射表。
|
||||
*
|
||||
* @returns breadcrumbs - 原始面包屑数据
|
||||
* @returns breadcrumbItems - 处理后的面包屑项(非末项渲染为可点击链接)
|
||||
*/
|
||||
export function useBreadcrumb() {
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
/**
|
||||
* 根据当前路径和记录路径,计算拼接后的路径
|
||||
* 根据父级已拼接路径与当前路由记录的 path,计算该层的完整跳转路径
|
||||
* 兼容子路由使用相对路径或绝对路径两种写法
|
||||
*/
|
||||
const buildPath = (currentPath: string, recordPath: string): string => {
|
||||
const buildPath = (parentPath: string, recordPath: string): string => {
|
||||
if (recordPath.startsWith('/')) {
|
||||
return recordPath;
|
||||
}
|
||||
return currentPath ? `${currentPath}/${recordPath}`.replace(/\/+/g, '/') : `/${recordPath}`;
|
||||
return `${parentPath}/${recordPath}`.replace(/\/+/g, '/');
|
||||
};
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
const matched = route.matched;
|
||||
const items: Array<{ title: string; path?: string }> = [];
|
||||
const items: Array<{ title: string; path: string }> = [];
|
||||
let currentPath = '';
|
||||
|
||||
for (const record of matched) {
|
||||
for (const record of route.matched) {
|
||||
currentPath = buildPath(currentPath, record.path);
|
||||
if (record.meta && record.meta.title) {
|
||||
if (record.meta?.title) {
|
||||
items.push({
|
||||
title: record.meta.title as string,
|
||||
path: currentPath,
|
||||
|
|
@ -38,25 +49,19 @@ export function useBreadcrumb() {
|
|||
key: index,
|
||||
title:
|
||||
item.path && index !== breadcrumbs.value.length - 1
|
||||
? h(
|
||||
? // 非末项:渲染为可点击链接
|
||||
h(
|
||||
'a',
|
||||
{
|
||||
onClick: (e: Event) => {
|
||||
e.preventDefault();
|
||||
// 最保守方案:只有当前路径包含 /bracket/ 且目标路径是 /events/bracket 时才保留 query
|
||||
// 也就是只有:/events/bracket/xxx → /events/bracket 时才保留
|
||||
const targetPath = item.path || '/';
|
||||
const isBracketChildPage =
|
||||
route.path.includes('/bracket/') && targetPath === '/events/bracket';
|
||||
router.push({
|
||||
path: targetPath,
|
||||
query: isBracketChildPage ? route.query : {},
|
||||
});
|
||||
router.push(item.path);
|
||||
},
|
||||
},
|
||||
item.title,
|
||||
)
|
||||
: item.title,
|
||||
: // 末项:仅显示文本,表示当前所在位置
|
||||
item.title,
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,57 +1,200 @@
|
|||
.container {
|
||||
min-height: 100vh;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.sider {
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.08);
|
||||
background: #fff !important;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
:deep(.ant-menu) {
|
||||
background: #fff;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
:deep(.ant-menu-item) {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
color: #595959;
|
||||
|
||||
&:hover {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-menu-item-selected) {
|
||||
position: relative;
|
||||
background: linear-gradient(90deg, #e6f7ff 0%, #bae7ff 100%) !important;
|
||||
color: #1890ff !important;
|
||||
font-weight: 500;
|
||||
|
||||
// 左侧高亮指示条,标识当前选中的菜单项
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-menu-submenu-title) {
|
||||
margin: 0;
|
||||
color: #595959;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-menu-submenu-selected > .ant-menu-submenu-title) {
|
||||
color: #1890ff !important;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 56px;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 24px;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #262626;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.logoIcon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: linear-gradient(135deg, #1890ff 0%, #722ed1 100%);
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logoText {
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #fff;
|
||||
padding: 0 16px;
|
||||
background: #ffffff !important;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 64px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
:deep(.ant-layout-header) {
|
||||
background: #ffffff !important;
|
||||
}
|
||||
}
|
||||
|
||||
.headerLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.headerRight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
}
|
||||
|
||||
.trigger {
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
padding: 0 12px;
|
||||
padding: 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #595959;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
color: #1677ff;
|
||||
color: #1890ff;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 16px;
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
min-height: 280px;
|
||||
.refreshIcon {
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #595959;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
.breadcrumb {
|
||||
margin-left: 12px;
|
||||
color: #8c8c8c;
|
||||
|
||||
:deep(.ant-breadcrumb-link) {
|
||||
color: #8c8c8c;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-breadcrumb-last) {
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
.headerIcon {
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
color: #595959;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.userInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.3s;
|
||||
|
||||
// &:hover {
|
||||
// background: #f5f5f5;
|
||||
// }
|
||||
}
|
||||
|
||||
.userName {
|
||||
color: #262626;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 0;
|
||||
background: #f0f2f5;
|
||||
min-height: calc(100vh - 64px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.pageContent {
|
||||
padding: 15px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,69 +1,171 @@
|
|||
import { computed, defineComponent, ref } from 'vue';
|
||||
import { useRouter, useRoute, RouterView } from 'vue-router';
|
||||
import { Layout, Menu } from 'ant-design-vue';
|
||||
import type { MenuProps } from 'ant-design-vue';
|
||||
import { Layout, Menu, Breadcrumb, Modal, Avatar, Space } from 'ant-design-vue';
|
||||
import { RouterView, useRouter } from 'vue-router';
|
||||
import {
|
||||
DashboardOutlined,
|
||||
InfoCircleOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
BellOutlined,
|
||||
FullscreenOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { useBasicLayout } from '../hooks/useBasicLayout';
|
||||
import { auth } from '@/hooks/useAuth';
|
||||
import styles from './BasicLayout.module.less';
|
||||
|
||||
const { Header, Sider, Content, Footer } = Layout;
|
||||
// 解构 Layout 组件
|
||||
const { Header, Sider, Content } = Layout;
|
||||
|
||||
/**
|
||||
* 基础布局:左侧导航 + 顶部栏 + 内容区 + 页脚
|
||||
* 基础布局组件
|
||||
*
|
||||
* @description
|
||||
* CPMS 运营平台的主布局结构,包含:
|
||||
* - 左侧侧边栏:Logo + 导航菜单
|
||||
* - 顶部 Header:菜单折叠按钮 + 面包屑 + 用户信息
|
||||
* - 内容区域:路由视图渲染区域
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 在路由中使用
|
||||
* {
|
||||
* path: '/',
|
||||
* component: BasicLayout,
|
||||
* children: [...]
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'BasicLayout',
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
// 使用布局 Hook 获取菜单和面包屑数据
|
||||
const { breadcrumbItems, menuItems, getActiveMenuKey, getOpenKeys, handleMenuClick } =
|
||||
useBasicLayout();
|
||||
|
||||
// 路由实例,用于退出登录后跳转到登录页
|
||||
const router = useRouter();
|
||||
|
||||
/**
|
||||
* 侧边栏折叠状态
|
||||
* true: 折叠状态(只显示图标)
|
||||
* false: 展开状态(显示图标+文字)
|
||||
*/
|
||||
const collapsed = ref(false);
|
||||
|
||||
const selectedKeys = computed<string[]>(() => [route.path]);
|
||||
|
||||
const menuItems: MenuProps['items'] = [
|
||||
{ key: '/dashboard', icon: () => <DashboardOutlined />, label: '工作台' },
|
||||
{ key: '/about', icon: () => <InfoCircleOutlined />, label: '关于' },
|
||||
];
|
||||
|
||||
const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
|
||||
router.push(key as string);
|
||||
};
|
||||
|
||||
/**
|
||||
* 切换侧边栏折叠/展开状态
|
||||
*/
|
||||
const toggleCollapsed = () => {
|
||||
collapsed.value = !collapsed.value;
|
||||
};
|
||||
|
||||
/**
|
||||
* 退出登录:二次确认后跳转到登录页(不调用接口)
|
||||
* - 点击用户名弹出确认弹框
|
||||
* - 确定 -> 跳转登录页
|
||||
* - 取消 -> 关闭弹框,不做任何处理
|
||||
*/
|
||||
const handleLogout = () => {
|
||||
Modal.confirm({
|
||||
title: '确认退出',
|
||||
content: '确定要退出当前账号吗?',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
// 清除本地登录态(不调用接口),否则路由守卫会把 /login 重定向回首页
|
||||
auth.logout();
|
||||
router.replace('/login');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return () => (
|
||||
<Layout class={styles.container}>
|
||||
<Sider class={styles.sider} collapsed={collapsed.value} trigger={null} collapsible>
|
||||
{/* 左侧侧边栏 */}
|
||||
<Sider
|
||||
class={styles.sider}
|
||||
collapsed={collapsed.value}
|
||||
trigger={null}
|
||||
collapsible
|
||||
width={220}
|
||||
>
|
||||
{/* Logo 区域 */}
|
||||
<div class={styles.logo}>
|
||||
<span class={styles.logoText}>{collapsed.value ? 'CPMS' : 'CPMS 运营平台'}</span>
|
||||
<div class={styles.logoIcon} />
|
||||
<span class={styles.logoText}>{collapsed.value ? 'CPMS' : 'CPMS运营平台'}</span>
|
||||
</div>
|
||||
|
||||
{/* 导航菜单 */}
|
||||
<Menu
|
||||
theme="dark"
|
||||
theme="light"
|
||||
mode="inline"
|
||||
selectedKeys={selectedKeys.value}
|
||||
selectedKeys={[getActiveMenuKey()]}
|
||||
openKeys={getOpenKeys()}
|
||||
items={menuItems}
|
||||
onClick={handleMenuClick}
|
||||
/>
|
||||
</Sider>
|
||||
|
||||
{/* 右侧内容区域 */}
|
||||
<Layout>
|
||||
<Header class={styles.header}>
|
||||
<span class={styles.trigger} onClick={toggleCollapsed}>
|
||||
{collapsed.value ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
</span>
|
||||
{/* 顶部 Header */}
|
||||
<Header class={styles.header} style={{ background: '#ffffff' }}>
|
||||
<div class={styles.headerLeft}>
|
||||
{/* 折叠/展开按钮 */}
|
||||
<span class={styles.trigger} onClick={toggleCollapsed}>
|
||||
{collapsed.value ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
</span>
|
||||
|
||||
{/* 刷新按钮 */}
|
||||
{/* <span class={styles.refreshIcon}>
|
||||
<ReloadOutlined />
|
||||
</span> */}
|
||||
|
||||
{/* 面包屑导航 */}
|
||||
{breadcrumbItems.value.length > 0 && (
|
||||
<Breadcrumb class={styles.breadcrumb}>
|
||||
{breadcrumbItems.value.map((item) => (
|
||||
<Breadcrumb.Item key={item.key}>{item.title}</Breadcrumb.Item>
|
||||
))}
|
||||
</Breadcrumb>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Header 右侧:用户信息 */}
|
||||
<div class={styles.headerRight}>
|
||||
<Space size="middle">
|
||||
{/* 搜索图标 */}
|
||||
{/* <span class={styles.headerIcon}>
|
||||
<SearchOutlined />
|
||||
</span> */}
|
||||
|
||||
{/* 通知图标 */}
|
||||
{/* <span class={styles.headerIcon}>
|
||||
<BellOutlined />
|
||||
</span> */}
|
||||
|
||||
{/* 全屏图标 */}
|
||||
{/* <span class={styles.headerIcon}>
|
||||
<FullscreenOutlined />
|
||||
</span> */}
|
||||
|
||||
{/* 用户信息:点击弹出退出登录确认 */}
|
||||
<div class={styles.userInfo} onClick={handleLogout}>
|
||||
<Avatar size="small" icon={<UserOutlined />} />
|
||||
<span class={styles.userName}>用户名</span>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
</Header>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<Content class={styles.content}>
|
||||
<RouterView />
|
||||
<div class={styles.pageContent}>
|
||||
{/* 路由视图:根据当前路由渲染对应页面 */}
|
||||
<RouterView />
|
||||
</div>
|
||||
</Content>
|
||||
|
||||
<Footer class={styles.footer}>CPMS 运营平台 ©{new Date().getFullYear()}</Footer>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'ant-design-vue/dist/reset.css';
|
|||
import App from './App';
|
||||
import router from './router';
|
||||
import './assets/styles/index.less';
|
||||
import './assets/iconfont/iconfont.css';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
import { routes } from './routes';
|
||||
import { auth } from '@/hooks/useAuth';
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.VITE_BASE_URL || '/'),
|
||||
history: createWebHashHistory(import.meta.env.VITE_BASE_URL || '/'),
|
||||
routes,
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,40 +1,23 @@
|
|||
import type { RouteRecordRaw } from 'vue-router';
|
||||
import {
|
||||
BarChartOutlined,
|
||||
UserOutlined,
|
||||
StarOutlined,
|
||||
FileTextOutlined,
|
||||
NotificationOutlined,
|
||||
BorderTopOutlined,
|
||||
BoxPlotOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { createRoute } from './utils';
|
||||
|
||||
/**
|
||||
* ====== 业务路由模块 ======
|
||||
* 实际项目中按模块拆分,如 eventsRoutes、walletRoutes 等,
|
||||
* 然后在下方 routes 数组的布局 children 中引入。
|
||||
* 参考示例(嵌套路由):
|
||||
*
|
||||
* const eventsRoutes = {
|
||||
* path: '/events',
|
||||
* name: 'EventsModule',
|
||||
* redirect: '/events/list',
|
||||
* meta: { title: '我的赛事', activeMenu: '/events' },
|
||||
* children: [
|
||||
* createRoute('list', () => import('@/pages/events/list'), { title: '赛事列表' }),
|
||||
* {
|
||||
* path: 'bracket',
|
||||
* name: 'BracketPage',
|
||||
* component: () => import('@/pages/events/bracket'),
|
||||
* meta: { title: '赛事详情' },
|
||||
* children: [
|
||||
* createRoute('player-management', () => import('@/pages/events/bracket/player-management'), { title: '选手管理' }),
|
||||
* createRoute('match-result', () => import('@/pages/events/bracket/match-result'), { title: '比赛结果' }),
|
||||
* ],
|
||||
* },
|
||||
* ],
|
||||
* };
|
||||
*/
|
||||
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
// ── 登录页(不经过布局)──
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/pages/login'),
|
||||
meta: { title: '登录' },
|
||||
meta: { title: '登录', hideInMenu: true },
|
||||
},
|
||||
|
||||
// ── 主布局(需要登录)──
|
||||
|
|
@ -42,17 +25,149 @@ export const routes: RouteRecordRaw[] = [
|
|||
path: '/',
|
||||
component: () => import('@/layouts/BasicLayout'),
|
||||
children: [
|
||||
// 业务模块路由在此添加,例如:eventsRoutes, walletRoutes
|
||||
|
||||
// 数据分析
|
||||
createRoute('/dashboard', () => import('@/views/Dashboard'), {
|
||||
title: '工作台',
|
||||
icon: 'DashboardOutlined',
|
||||
title: '数据分析',
|
||||
icon: BarChartOutlined,
|
||||
}),
|
||||
|
||||
// 场馆列表(分组)
|
||||
{
|
||||
path: '/venue',
|
||||
name: 'Venue',
|
||||
redirect: '/venue/list',
|
||||
meta: {
|
||||
title: '场馆列表',
|
||||
icon: 'icon-changguan1',
|
||||
activeMenu: '/venue',
|
||||
},
|
||||
children: [
|
||||
createRoute('/venue/list', () => import('@/views/venue/list'), {
|
||||
title: '馆客多',
|
||||
}),
|
||||
createRoute('/venue/records', () => import('@/views/venue/records'), {
|
||||
title: '收录',
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
// C端用户列表
|
||||
createRoute('/users', () => import('@/views/Dashboard'), {
|
||||
title: 'C端用户列表',
|
||||
icon: UserOutlined,
|
||||
}),
|
||||
// 点评管理
|
||||
createRoute('/reviews', () => import('@/views/Dashboard'), {
|
||||
title: '点评管理',
|
||||
icon: StarOutlined,
|
||||
}),
|
||||
// 广告位管理
|
||||
createRoute('/ads', () => import('@/views/Dashboard'), {
|
||||
title: '广告位管理',
|
||||
icon: NotificationOutlined,
|
||||
}),
|
||||
// 资讯管理
|
||||
createRoute('/news', () => import('@/views/Dashboard'), {
|
||||
title: '资讯管理',
|
||||
icon: FileTextOutlined,
|
||||
}),
|
||||
// 申请入驻管理
|
||||
createRoute('/apply', () => import('@/views/Dashboard'), {
|
||||
title: '申请入驻管理',
|
||||
icon: UserOutlined,
|
||||
}),
|
||||
// 反馈管理
|
||||
createRoute('/feedback', () => import('@/views/Dashboard'), {
|
||||
title: '反馈管理',
|
||||
icon: BoxPlotOutlined,
|
||||
}),
|
||||
|
||||
// 活动管理(分组)
|
||||
{
|
||||
path: '/activity',
|
||||
name: 'Activity',
|
||||
redirect: '/activity/list',
|
||||
meta: { title: '活动管理', icon: 'icon-rili', activeMenu: '/activity' },
|
||||
children: [
|
||||
createRoute('/activity/list', () => import('@/views/Dashboard'), {
|
||||
title: '活动列表',
|
||||
}),
|
||||
createRoute('/activity/categories', () => import('@/views/Dashboard'), {
|
||||
title: '活动分类',
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
// 场馆设置(分组)
|
||||
{
|
||||
path: '/venue-settings',
|
||||
name: 'VenueSettings',
|
||||
redirect: '/venue-settings/basic',
|
||||
meta: {
|
||||
title: '场馆设置',
|
||||
icon: BorderTopOutlined,
|
||||
activeMenu: '/venue-settings',
|
||||
},
|
||||
children: [
|
||||
createRoute('/venue-settings/basic', () => import('@/views/Dashboard'), {
|
||||
title: '基本设置',
|
||||
}),
|
||||
createRoute('/venue-settings/service', () => import('@/views/Dashboard'), {
|
||||
title: '服务设置',
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
// 钱包管理(分组)
|
||||
{
|
||||
path: '/wallet',
|
||||
name: 'Wallet',
|
||||
redirect: '/wallet/records',
|
||||
meta: { title: '钱包管理', icon: 'icon-qianbao', activeMenu: '/wallet' },
|
||||
children: [
|
||||
createRoute('/wallet/records', () => import('@/views/Dashboard'), {
|
||||
title: '交易记录',
|
||||
}),
|
||||
createRoute('/wallet/withdraw', () => import('@/views/Dashboard'), {
|
||||
title: '提现管理',
|
||||
}),
|
||||
createRoute('/wallet/recharge', () => import('@/views/Dashboard'), {
|
||||
title: '充值管理',
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
// 系统管理(分组)
|
||||
{
|
||||
path: '/system',
|
||||
name: 'System',
|
||||
redirect: '/system/users',
|
||||
meta: {
|
||||
title: '系统管理',
|
||||
icon: SafetyCertificateOutlined,
|
||||
activeMenu: '/system',
|
||||
},
|
||||
children: [
|
||||
createRoute('/system/users', () => import('@/views/Dashboard'), {
|
||||
title: '用户管理',
|
||||
}),
|
||||
createRoute('/system/roles', () => import('@/views/Dashboard'), {
|
||||
title: '角色管理',
|
||||
}),
|
||||
createRoute('/system/permissions', () => import('@/views/Dashboard'), {
|
||||
title: '权限管理',
|
||||
}),
|
||||
createRoute('/system/logs', () => import('@/views/Dashboard'), {
|
||||
title: '操作日志',
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
// 关于(不在侧边栏显示)
|
||||
createRoute('/about', () => import('@/views/About'), {
|
||||
title: '关于',
|
||||
icon: 'InfoCircleOutlined',
|
||||
hideInMenu: true,
|
||||
}),
|
||||
|
||||
// 默认重定向
|
||||
{ path: '/', redirect: '/dashboard' },
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,20 +1,25 @@
|
|||
import { h, type Component, type VNode } from 'vue';
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
/** 路由 meta 类型 */
|
||||
export type AppMeta = {
|
||||
/** 页面 / 菜单标题,同时用于面包屑 */
|
||||
title: string;
|
||||
icon?: string;
|
||||
/**
|
||||
* 菜单图标(仅一级菜单需要),支持两种写法:
|
||||
* - iconfont 的 class 名,如 'icon-changguan1'
|
||||
* - 组件,如 ant-design 图标 BarChartOutlined
|
||||
*/
|
||||
icon?: Component | string;
|
||||
/** 是否在侧边栏菜单中隐藏(详情页等) */
|
||||
hideInMenu?: boolean;
|
||||
/** 高亮的菜单 key(路径不等于菜单 key 的场景,如详情页) */
|
||||
activeMenu?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 快速创建路由记录
|
||||
* 根据路径自动生成 PascalCase 路由 name
|
||||
*
|
||||
* @example
|
||||
* createRoute('list', () => import('@/pages/home'), { title: '赛事列表' })
|
||||
* // => { path: 'list', name: 'List', meta: {...}, component: ... }
|
||||
*/
|
||||
export function createRoute(
|
||||
path: string,
|
||||
|
|
@ -38,3 +43,47 @@ export function createRoute(
|
|||
children,
|
||||
};
|
||||
}
|
||||
|
||||
/** 侧边栏菜单项(ant-design-vue Menu 的 items 结构) */
|
||||
export interface MenuItem {
|
||||
key: string;
|
||||
label: string;
|
||||
icon?: VNode;
|
||||
children?: MenuItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路由表生成侧边栏菜单
|
||||
*
|
||||
* @description
|
||||
* 菜单完全由路由派生,路由是「菜单 / 面包屑」唯一的数据源:
|
||||
* - 仅收录带 meta.title 且未标记 hideInMenu 的路由;
|
||||
* - 有子路由的生成子菜单(父级本身不跳转,点击展开);
|
||||
* - 图标取自 meta.icon。
|
||||
*
|
||||
* @param routes 需要遍历的路由数组(通常传入主布局的 children)
|
||||
*/
|
||||
export function generateMenu(routes: RouteRecordRaw[] = []): MenuItem[] {
|
||||
const items: MenuItem[] = [];
|
||||
|
||||
for (const route of routes) {
|
||||
const meta = route.meta as AppMeta | undefined;
|
||||
if (!meta?.title || meta.hideInMenu) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const children = generateMenu(route.children);
|
||||
const item: MenuItem = { key: route.path, label: meta.title };
|
||||
if (meta.icon) {
|
||||
// 字符串 -> iconfont 图标(<i class="iconfont icon-xxx" />);组件 -> 直接渲染
|
||||
item.icon =
|
||||
typeof meta.icon === 'string' ? h('i', { class: ['iconfont', meta.icon] }) : h(meta.icon);
|
||||
}
|
||||
if (children.length) {
|
||||
item.children = children;
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ export interface SelectOption {
|
|||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
title?: string;
|
||||
icon?: string;
|
||||
/** 菜单图标:iconfont 的 class 名(字符串) 或 组件(如 antd 图标) */
|
||||
icon?: import('vue').Component | string;
|
||||
/** 是否需要登录 */
|
||||
requiresAuth?: boolean;
|
||||
/** 是否在菜单中隐藏 */
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
.container {
|
||||
// 容器
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 24px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin-top: 16px;
|
||||
|
||||
ul {
|
||||
margin: 12px 0 0;
|
||||
padding-left: 20px;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 2px 8px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
import { defineComponent } from 'vue';
|
||||
import { Card, Col, Row, Statistic } from 'ant-design-vue';
|
||||
import styles from './Dashboard.module.less';
|
||||
|
||||
interface StatItem {
|
||||
title: string;
|
||||
value: number;
|
||||
suffix: string;
|
||||
precision?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作台 / 仪表盘
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'Dashboard',
|
||||
setup() {
|
||||
const stats: StatItem[] = [
|
||||
{ title: '今日访问', value: 1280, suffix: '次' },
|
||||
{ title: '活跃用户', value: 368, suffix: '人' },
|
||||
{ title: '订单总数', value: 9920, suffix: '单' },
|
||||
{ title: '处理率', value: 96.8, suffix: '%', precision: 1 },
|
||||
];
|
||||
|
||||
return () => (
|
||||
<div class={styles.container}>
|
||||
<h2 class={styles.title}>工作台</h2>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{stats.map((item) => (
|
||||
<Col key={item.title} xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title={item.title}
|
||||
value={item.value}
|
||||
suffix={item.suffix}
|
||||
precision={item.precision}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
<Card class={styles.panel} title="项目说明">
|
||||
<p>
|
||||
欢迎使用 CPMS 运营平台。这是一个基于 Vue 3 + Vite + TypeScript + Ant Design Vue
|
||||
搭建的中后台管理项目骨架,采用 TSX 写法,你可以在此基础上继续开发业务功能。
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
开发命令:<code>npm run dev</code>
|
||||
</li>
|
||||
<li>
|
||||
构建(测试环境):<code>npm run build:test</code>
|
||||
</li>
|
||||
<li>
|
||||
构建(生产环境):<code>npm run build:prod</code>
|
||||
</li>
|
||||
<li>
|
||||
代码检查:<code>npm run lint</code>
|
||||
</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
import { defineComponent, reactive, ref, watch } from 'vue';
|
||||
import { Modal, Form, Input, Select, message } from 'ant-design-vue';
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import { saveVenue, statusMap, type VenueSaveParams } from '../model';
|
||||
|
||||
interface AddVenueModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增场馆弹框
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'AddVenueModal',
|
||||
props: {
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onClose: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
onSuccess: {
|
||||
type: Function,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
setup(props: AddVenueModalProps) {
|
||||
const labelCol = { span: 5 };
|
||||
const wrapperCol = { span: 18 };
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref();
|
||||
// 提交中状态
|
||||
const submitting = ref(false);
|
||||
|
||||
// 表单数据初始值
|
||||
const getInitialForm = (): VenueSaveParams => ({
|
||||
name: '',
|
||||
type: '',
|
||||
contact: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
status: 'active',
|
||||
});
|
||||
const formState = reactive<VenueSaveParams>(getInitialForm());
|
||||
|
||||
// 校验规则
|
||||
const rules: Record<string, Rule[]> = {
|
||||
name: [{ required: true, message: '请输入场馆名称', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请输入场馆类型', trigger: 'blur' }],
|
||||
contact: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
|
||||
phone: [
|
||||
{ required: true, message: '请输入联系电话', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^1[3-9]\d{9}$/,
|
||||
message: '手机号格式不正确',
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
|
||||
};
|
||||
|
||||
// 打开时重置表单
|
||||
watch(
|
||||
() => props.open,
|
||||
(open) => {
|
||||
if (open) {
|
||||
Object.assign(formState, getInitialForm());
|
||||
formRef.value?.clearValidate?.();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/** 确定:校验 -> 提交 -> 成功回调 */
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
await formRef.value?.validate();
|
||||
} catch {
|
||||
// 校验不通过,直接返回
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
await saveVenue({ ...formState });
|
||||
message.success('新增成功');
|
||||
props.onSuccess?.();
|
||||
props.onClose();
|
||||
} catch {
|
||||
message.error('新增失败,请重试');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<Modal
|
||||
open={props.open}
|
||||
title="新增场馆"
|
||||
width={560}
|
||||
centered
|
||||
destroyOnClose
|
||||
confirmLoading={submitting.value}
|
||||
onOk={handleOk}
|
||||
onCancel={() => props.onClose()}
|
||||
>
|
||||
<Form
|
||||
ref={formRef}
|
||||
model={formState}
|
||||
rules={rules}
|
||||
labelCol={labelCol}
|
||||
wrapperCol={wrapperCol}
|
||||
style={{ marginTop: '8px' }}
|
||||
>
|
||||
<Form.Item label="场馆名称" name="name">
|
||||
<Input
|
||||
value={formState.name}
|
||||
onChange={(e: any) => {
|
||||
formState.name = e.target.value;
|
||||
}}
|
||||
placeholder="请输入场馆名称"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="场馆类型" name="type">
|
||||
<Input
|
||||
value={formState.type}
|
||||
onChange={(e: any) => {
|
||||
formState.type = e.target.value;
|
||||
}}
|
||||
placeholder="请输入场馆类型"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="联系人" name="contact">
|
||||
<Input
|
||||
value={formState.contact}
|
||||
onChange={(e: any) => {
|
||||
formState.contact = e.target.value;
|
||||
}}
|
||||
placeholder="请输入联系人"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="联系电话" name="phone">
|
||||
<Input
|
||||
value={formState.phone}
|
||||
onChange={(e: any) => {
|
||||
formState.phone = e.target.value;
|
||||
}}
|
||||
placeholder="请输入联系电话"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" name="status">
|
||||
<Select
|
||||
value={formState.status}
|
||||
onChange={(val: any) => {
|
||||
formState.status = val;
|
||||
}}
|
||||
placeholder="请选择状态"
|
||||
>
|
||||
{Object.entries(statusMap).map(([value, { text }]) => (
|
||||
<Select.Option key={value} value={value}>
|
||||
{text}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="地址" name="address">
|
||||
<Input.TextArea
|
||||
value={formState.address}
|
||||
onChange={(e: any) => {
|
||||
formState.address = e.target.value;
|
||||
}}
|
||||
placeholder="请输入地址"
|
||||
rows={3}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 64px - 30px);
|
||||
}
|
||||
|
||||
.filterCard {
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
|
||||
:global(.ant-card-body) {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.filterRow {
|
||||
:global(.ant-form) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
row-gap: 12px;
|
||||
column-gap: 16px;
|
||||
}
|
||||
|
||||
:global(.ant-form-item) {
|
||||
flex-shrink: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tableCard {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
|
||||
:global(.ant-card-body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tableBody {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.tableHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.tableFooter {
|
||||
flex-shrink: 0;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.paginationText {
|
||||
color: #595959;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
import { defineComponent } from 'vue';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Input,
|
||||
Select,
|
||||
Button,
|
||||
Space,
|
||||
Tag,
|
||||
Popconfirm,
|
||||
Tooltip,
|
||||
Pagination,
|
||||
Form,
|
||||
} from 'ant-design-vue';
|
||||
import {
|
||||
SearchOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
EyeOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { useDashboardModel, type VenueItem } from './model';
|
||||
import AddVenueModal from './components/AddVenueModal';
|
||||
import styles from './index.module.less';
|
||||
|
||||
/**
|
||||
* 数据分析 / 场馆列表
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'Dashboard',
|
||||
setup() {
|
||||
const {
|
||||
tableData,
|
||||
currentPage,
|
||||
formRef,
|
||||
formState,
|
||||
statusMap,
|
||||
addModalOpen,
|
||||
openAddModal,
|
||||
closeAddModal,
|
||||
handleReset,
|
||||
fetchVenueList,
|
||||
} = useDashboardModel();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '场馆名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '场馆类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '联系人',
|
||||
dataIndex: 'contact',
|
||||
key: 'contact',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '地址',
|
||||
dataIndex: 'address',
|
||||
key: 'address',
|
||||
// 开启文本溢出省略 + 悬浮 tooltip
|
||||
customRender: ({ record }: { record: VenueItem }) => (
|
||||
<Tooltip title={record.address}>
|
||||
<div class="match-ellipsis-single">{record.address}</div>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
customRender: ({ record }: { record: VenueItem }) => {
|
||||
const status = statusMap[record.status] || {
|
||||
color: 'default',
|
||||
text: '未知',
|
||||
};
|
||||
return <Tag color={status.color}>{status.text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'avatar',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 230,
|
||||
fixed: 'right' as const,
|
||||
customRender: () => (
|
||||
<Space size="small">
|
||||
<Button type="link" size="small" icon={<EyeOutlined />}>
|
||||
查看
|
||||
</Button>
|
||||
{/* <i class="iconfont icon-fenzutitle" style={{ fontSize: '18px', color: '#1677ff' }} /> */}
|
||||
<Button type="link" size="small" icon={<EditOutlined />}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm title="确定要删除吗?" okText="确定" cancelText="取消">
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return () => (
|
||||
<div class={styles.container}>
|
||||
<Card class={styles.filterCard}>
|
||||
<div class={styles.filterRow}>
|
||||
<Form ref={formRef} layout="inline">
|
||||
<Form.Item name="name">
|
||||
<Input
|
||||
value={formState.name}
|
||||
onChange={(e: any) => {
|
||||
formState.name = e.target.value;
|
||||
}}
|
||||
placeholder="请输入场馆名称"
|
||||
prefix={<SearchOutlined />}
|
||||
style={{ width: '200px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="场馆类型" name="type">
|
||||
<Select
|
||||
value={formState.type}
|
||||
onChange={(val: any) => {
|
||||
formState.type = val;
|
||||
}}
|
||||
placeholder="请选择场馆类型"
|
||||
style={{ width: '160px' }}
|
||||
allowClear={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" name="status">
|
||||
<Select
|
||||
value={formState.status}
|
||||
onChange={(val: any) => {
|
||||
formState.status = val;
|
||||
}}
|
||||
placeholder="请选择状态"
|
||||
style={{ width: '140px' }}
|
||||
allowClear={false}
|
||||
>
|
||||
{Object.entries(statusMap).map(([value, { text }]) => (
|
||||
<Select.Option key={value} value={value}>
|
||||
{text}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="联系人" name="contact">
|
||||
<Input
|
||||
value={formState.contact}
|
||||
onChange={(e: any) => {
|
||||
formState.contact = e.target.value;
|
||||
}}
|
||||
placeholder="请输入联系人"
|
||||
style={{ width: '150px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="联系电话" name="phone">
|
||||
<Input
|
||||
value={formState.phone}
|
||||
onChange={(e: any) => {
|
||||
formState.phone = e.target.value;
|
||||
}}
|
||||
placeholder="请输入联系电话"
|
||||
style={{ width: '150px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary">查询</Button>
|
||||
<Button onClick={handleReset}>重置</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class={styles.tableCard}>
|
||||
<div class={styles.tableHeader}>
|
||||
<span>{/* 场馆列表 */}</span>
|
||||
<Button
|
||||
type="primary"
|
||||
/* icon={<i class="iconfont icon-tianjia" />} */
|
||||
icon={<PlusOutlined />}
|
||||
onClick={openAddModal}
|
||||
>
|
||||
新增
|
||||
</Button>
|
||||
</div>
|
||||
<div class={styles.tableBody}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData.value}
|
||||
pagination={false}
|
||||
scroll={{ x: 1400 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={styles.tableFooter}>
|
||||
<Pagination
|
||||
current={currentPage.value}
|
||||
onChange={(page: number) => {
|
||||
currentPage.value = page;
|
||||
}}
|
||||
total={500}
|
||||
showSizeChanger
|
||||
showQuickJumper
|
||||
showTotal={(total: number) => `共 ${total} 条`}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 新增场馆弹框 */}
|
||||
<AddVenueModal
|
||||
open={addModalOpen.value}
|
||||
onClose={closeAddModal}
|
||||
onSuccess={fetchVenueList}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './useDashboardModel';
|
||||
export * from './services';
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
import { get, post } from '@/utils/request';
|
||||
|
||||
/** 场馆列表行数据 */
|
||||
export interface VenueItem {
|
||||
key: string;
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
contact: string;
|
||||
status: string;
|
||||
phone: string;
|
||||
address: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
/** 场馆状态映射:状态值 -> 颜色 / 文案 */
|
||||
export const statusMap: Record<string, { color: string; text: string }> = {
|
||||
active: { color: 'green', text: '正常' },
|
||||
inactive: { color: 'red', text: '停用' },
|
||||
pending: { color: 'orange', text: '待审' },
|
||||
};
|
||||
|
||||
/** 场馆列表查询参数 */
|
||||
export interface VenueListParams {
|
||||
page: number;
|
||||
size: number;
|
||||
name?: string;
|
||||
type?: string;
|
||||
status?: string;
|
||||
contact?: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
const venueListApi = '/venue/page';
|
||||
|
||||
/**
|
||||
* 场馆列表接口
|
||||
* @description 后端接口就绪后,在 useDashboardModel 中启用替换 mock 数据
|
||||
*/
|
||||
export const getVenueList = (params: VenueListParams) => {
|
||||
return get(venueListApi, params);
|
||||
};
|
||||
|
||||
/** 新增场馆参数 */
|
||||
export interface VenueSaveParams {
|
||||
name: string;
|
||||
type: string;
|
||||
contact: string;
|
||||
phone: string;
|
||||
address: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const venueSaveApi = '/venue/save';
|
||||
|
||||
/** 新增场馆接口 */
|
||||
export const saveVenue = (params: VenueSaveParams) => {
|
||||
return post(venueSaveApi, params);
|
||||
};
|
||||
|
||||
/** 临时 mock 数据(接口对接前占位展示) */
|
||||
export const mockVenueList: VenueItem[] = [
|
||||
{
|
||||
key: '1',
|
||||
id: 'V001',
|
||||
name: '健身中心',
|
||||
type: '健身馆',
|
||||
contact: '张三',
|
||||
status: 'active',
|
||||
phone: '13999999999',
|
||||
address: '北京市朝阳区xxx路xxx号',
|
||||
createTime: '2024-01-15 10:30:00',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
id: 'V002',
|
||||
name: '篮球公园',
|
||||
type: '篮球馆',
|
||||
contact: '李四',
|
||||
status: 'active',
|
||||
phone: '13999999999',
|
||||
address: '上海市浦东新区xxx路xxx号',
|
||||
createTime: '2024-01-16 14:20:00',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
id: 'V003',
|
||||
name: '游泳馆',
|
||||
type: '游泳馆',
|
||||
contact: '王五',
|
||||
status: 'inactive',
|
||||
phone: '13999999999',
|
||||
address: '广州市天河区xxx路xxx号',
|
||||
createTime: '2024-01-17 09:15:00',
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
id: 'V004',
|
||||
name: '羽毛球馆',
|
||||
type: '羽毛球馆',
|
||||
contact: '马六',
|
||||
status: 'active',
|
||||
phone: '13999999999',
|
||||
address: '深圳市南山区xxx路xxx号',
|
||||
createTime: '2024-01-18 16:45:00',
|
||||
},
|
||||
{
|
||||
key: '5',
|
||||
id: 'V005',
|
||||
name: '网球中心',
|
||||
type: '网球馆',
|
||||
contact: '冯七',
|
||||
status: 'pending',
|
||||
phone: '13999999999',
|
||||
address:
|
||||
'杭州市西湖区xxx路xxx号杭州市西湖区xxx路xxx号杭州市西湖区xxx路xxx号杭州市西湖区xxx路xxx号11',
|
||||
createTime: '2024-01-19 11:00:00',
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { reactive, ref } from 'vue';
|
||||
import { getVenueList, mockVenueList, statusMap, type VenueItem } from './services';
|
||||
|
||||
/** 筛选表单字段 */
|
||||
export interface VenueFilterForm {
|
||||
name: string;
|
||||
type?: string;
|
||||
status?: string;
|
||||
contact: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard(场馆列表)页面业务逻辑
|
||||
*
|
||||
* @description
|
||||
* 页面的状态、数据、事件处理都集中在这里,index.tsx 只负责渲染。
|
||||
*/
|
||||
export function useDashboardModel() {
|
||||
// 表格数据(暂用 mock,接口就绪后由 fetchVenueList 填充)
|
||||
const tableData = ref<VenueItem[]>(mockVenueList);
|
||||
// 当前页码
|
||||
const currentPage = ref(1);
|
||||
// 筛选表单实例
|
||||
const formRef = ref();
|
||||
// 筛选表单数据
|
||||
const formState = reactive<VenueFilterForm>({
|
||||
name: '',
|
||||
type: undefined,
|
||||
status: undefined,
|
||||
contact: '',
|
||||
phone: '',
|
||||
});
|
||||
|
||||
// 新增场馆弹框显隐
|
||||
const addModalOpen = ref(false);
|
||||
/** 打开新增弹框 */
|
||||
const openAddModal = () => {
|
||||
addModalOpen.value = true;
|
||||
};
|
||||
/** 关闭新增弹框 */
|
||||
const closeAddModal = () => {
|
||||
addModalOpen.value = false;
|
||||
};
|
||||
|
||||
/** 重置筛选表单 */
|
||||
const handleReset = () => {
|
||||
formRef.value?.resetFields();
|
||||
Object.assign(formState, {
|
||||
name: '',
|
||||
type: undefined,
|
||||
status: undefined,
|
||||
contact: '',
|
||||
phone: '',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询场馆列表
|
||||
* @description 后端接口就绪后启用,替换默认的 mock 数据
|
||||
*/
|
||||
const fetchVenueList = async () => {
|
||||
const res: any = await getVenueList({
|
||||
page: currentPage.value,
|
||||
size: 10,
|
||||
...formState,
|
||||
});
|
||||
tableData.value = res?.data?.list ?? [];
|
||||
};
|
||||
|
||||
return {
|
||||
tableData,
|
||||
currentPage,
|
||||
formRef,
|
||||
formState,
|
||||
statusMap,
|
||||
addModalOpen,
|
||||
openAddModal,
|
||||
closeAddModal,
|
||||
handleReset,
|
||||
fetchVenueList,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 64px - 30px);
|
||||
}
|
||||
|
||||
.filterCard {
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
|
||||
:global(.ant-card-body) {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.filterRow {
|
||||
:global(.ant-form) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
row-gap: 12px;
|
||||
column-gap: 16px;
|
||||
}
|
||||
|
||||
:global(.ant-form-item) {
|
||||
flex-shrink: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tableCard {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
|
||||
:global(.ant-card-body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tableBody {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.tableFooter {
|
||||
flex-shrink: 0;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
import { defineComponent } from 'vue';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Input,
|
||||
Select,
|
||||
Button,
|
||||
Space,
|
||||
Tag,
|
||||
Popconfirm,
|
||||
Pagination,
|
||||
Form,
|
||||
} from 'ant-design-vue';
|
||||
import { SearchOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons-vue';
|
||||
import { useVenueListModel, type VenueListItem } from './model';
|
||||
import styles from './index.module.less';
|
||||
|
||||
/**
|
||||
* 馆客多 / 场馆列表
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'VenueList',
|
||||
setup() {
|
||||
const { tableData, currentPage, formRef, formState, statusMap, handleReset } =
|
||||
useVenueListModel();
|
||||
|
||||
const columns = [
|
||||
{ title: '序号', dataIndex: 'id', key: 'id', width: 100 },
|
||||
{ title: '场馆名称', dataIndex: 'name', key: 'name', width: 160 },
|
||||
{ title: '场馆类型', dataIndex: 'type', key: 'type', width: 140 },
|
||||
{ title: '联系人', dataIndex: 'contact', key: 'contact', width: 120 },
|
||||
{ title: '联系电话', dataIndex: 'phone', key: 'phone', width: 160 },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
customRender: ({ record }: { record: VenueListItem }) => {
|
||||
const status = statusMap[record.status] || {
|
||||
color: 'default',
|
||||
text: '未知',
|
||||
};
|
||||
return <Tag color={status.color}>{status.text}</Tag>;
|
||||
},
|
||||
},
|
||||
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 180 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 230,
|
||||
fixed: 'right' as const,
|
||||
customRender: () => (
|
||||
<Space size="small">
|
||||
<Button type="link" size="small" icon={<EyeOutlined />}>
|
||||
查看
|
||||
</Button>
|
||||
<Button type="link" size="small" icon={<EditOutlined />}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm title="确定要删除吗?" okText="确定" cancelText="取消">
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return () => (
|
||||
<div class={styles.container}>
|
||||
<Card class={styles.filterCard}>
|
||||
<div class={styles.filterRow}>
|
||||
<Form ref={formRef} layout="inline">
|
||||
<Form.Item name="name">
|
||||
<Input
|
||||
value={formState.name}
|
||||
onChange={(e: any) => {
|
||||
formState.name = e.target.value;
|
||||
}}
|
||||
placeholder="请输入场馆名称"
|
||||
prefix={<SearchOutlined />}
|
||||
style={{ width: '200px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" name="status">
|
||||
<Select
|
||||
value={formState.status}
|
||||
onChange={(val: any) => {
|
||||
formState.status = val;
|
||||
}}
|
||||
placeholder="请选择状态"
|
||||
style={{ width: '140px' }}
|
||||
allowClear={false}
|
||||
>
|
||||
{Object.entries(statusMap).map(([value, { text }]) => (
|
||||
<Select.Option key={value} value={value}>
|
||||
{text}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary">查询</Button>
|
||||
<Button onClick={handleReset}>重置</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class={styles.tableCard}>
|
||||
<div class={styles.tableBody}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData.value}
|
||||
pagination={false}
|
||||
scroll={{ x: 1200 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={styles.tableFooter}>
|
||||
<Pagination
|
||||
current={currentPage.value}
|
||||
onChange={(page: number) => {
|
||||
currentPage.value = page;
|
||||
}}
|
||||
total={500}
|
||||
showSizeChanger
|
||||
showQuickJumper
|
||||
showTotal={(total: number) => `共 ${total} 条`}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './useVenueListModel';
|
||||
export * from './services';
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { get } from '@/utils/request';
|
||||
|
||||
/** 场馆列表行数据 */
|
||||
export interface VenueListItem {
|
||||
key: string;
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
contact: string;
|
||||
status: string;
|
||||
phone: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
/** 场馆状态映射:状态值 -> 颜色 / 文案 */
|
||||
export const statusMap: Record<string, { color: string; text: string }> = {
|
||||
active: { color: 'green', text: '正常' },
|
||||
inactive: { color: 'red', text: '停用' },
|
||||
pending: { color: 'orange', text: '待审' },
|
||||
};
|
||||
|
||||
/** 场馆列表查询参数 */
|
||||
export interface VenueListParams {
|
||||
page: number;
|
||||
size: number;
|
||||
name?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
const venueListApi = '/venue/list';
|
||||
|
||||
/**
|
||||
* 馆客多-场馆列表接口
|
||||
* @description 后端接口就绪后,在 useVenueListModel 中启用替换 mock 数据
|
||||
*/
|
||||
export const getVenueList = (params: VenueListParams) => {
|
||||
return get(venueListApi, params);
|
||||
};
|
||||
|
||||
/** 临时 mock 数据(接口对接前占位展示) */
|
||||
export const mockVenueList: VenueListItem[] = [
|
||||
{
|
||||
key: '1',
|
||||
id: 'V001',
|
||||
name: '健身中心',
|
||||
type: '健身馆',
|
||||
contact: '张三',
|
||||
status: 'active',
|
||||
phone: '13999999999',
|
||||
createTime: '2024-01-15 10:30:00',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
id: 'V002',
|
||||
name: '篮球公园',
|
||||
type: '篮球馆',
|
||||
contact: '李四',
|
||||
status: 'active',
|
||||
phone: '13999999999',
|
||||
createTime: '2024-01-16 14:20:00',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
id: 'V003',
|
||||
name: '游泳馆',
|
||||
type: '游泳馆',
|
||||
contact: '王五',
|
||||
status: 'inactive',
|
||||
phone: '13999999999',
|
||||
createTime: '2024-01-17 09:15:00',
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { reactive, ref } from 'vue';
|
||||
import { getVenueList, mockVenueList, statusMap, type VenueListItem } from './services';
|
||||
|
||||
/** 筛选表单字段 */
|
||||
export interface VenueListFilterForm {
|
||||
name: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 馆客多(场馆列表)页面业务逻辑
|
||||
*/
|
||||
export function useVenueListModel() {
|
||||
// 表格数据(暂用 mock,接口就绪后由 fetchList 填充)
|
||||
const tableData = ref<VenueListItem[]>(mockVenueList);
|
||||
// 当前页码
|
||||
const currentPage = ref(1);
|
||||
// 筛选表单实例
|
||||
const formRef = ref();
|
||||
// 筛选表单数据
|
||||
const formState = reactive<VenueListFilterForm>({
|
||||
name: '',
|
||||
status: undefined,
|
||||
});
|
||||
|
||||
/** 重置筛选表单 */
|
||||
const handleReset = () => {
|
||||
formRef.value?.resetFields();
|
||||
Object.assign(formState, { name: '', status: undefined });
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询场馆列表
|
||||
* @description 后端接口就绪后启用,替换默认的 mock 数据
|
||||
*/
|
||||
const fetchList = async () => {
|
||||
const res: any = await getVenueList({
|
||||
page: currentPage.value,
|
||||
size: 10,
|
||||
...formState,
|
||||
});
|
||||
tableData.value = res?.data?.list ?? [];
|
||||
};
|
||||
|
||||
return {
|
||||
tableData,
|
||||
currentPage,
|
||||
formRef,
|
||||
formState,
|
||||
statusMap,
|
||||
handleReset,
|
||||
fetchList,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 64px - 30px);
|
||||
}
|
||||
|
||||
.filterCard {
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
|
||||
:global(.ant-card-body) {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.filterRow {
|
||||
:global(.ant-form) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
row-gap: 12px;
|
||||
column-gap: 16px;
|
||||
}
|
||||
|
||||
:global(.ant-form-item) {
|
||||
flex-shrink: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tableCard {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
|
||||
:global(.ant-card-body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tableBody {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.tableFooter {
|
||||
flex-shrink: 0;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
import { defineComponent } from 'vue';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Input,
|
||||
Select,
|
||||
Button,
|
||||
Space,
|
||||
Tag,
|
||||
Popconfirm,
|
||||
Pagination,
|
||||
Form,
|
||||
} from 'ant-design-vue';
|
||||
import { SearchOutlined, EyeOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons-vue';
|
||||
import { useVenueRecordsModel, type VenueRecordItem } from './model';
|
||||
import styles from './index.module.less';
|
||||
|
||||
/**
|
||||
* 收录 / 收录记录列表
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'VenueRecords',
|
||||
setup() {
|
||||
const { tableData, currentPage, formRef, formState, statusMap, handleReset } =
|
||||
useVenueRecordsModel();
|
||||
|
||||
const columns = [
|
||||
{ title: '序号', dataIndex: 'id', key: 'id', width: 100 },
|
||||
{ title: '场馆名称', dataIndex: 'venueName', key: 'venueName', width: 180 },
|
||||
{ title: '来源', dataIndex: 'source', key: 'source', width: 140 },
|
||||
{
|
||||
title: '收录状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 120,
|
||||
customRender: ({ record }: { record: VenueRecordItem }) => {
|
||||
const status = statusMap[record.status] || {
|
||||
color: 'default',
|
||||
text: '未知',
|
||||
};
|
||||
return <Tag color={status.color}>{status.text}</Tag>;
|
||||
},
|
||||
},
|
||||
{ title: '收录时间', dataIndex: 'recordTime', key: 'recordTime', width: 180 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 240,
|
||||
fixed: 'right' as const,
|
||||
customRender: () => (
|
||||
<Space size="small">
|
||||
<Button type="link" size="small" icon={<EyeOutlined />}>
|
||||
查看
|
||||
</Button>
|
||||
<Button type="link" size="small" icon={<CheckOutlined />}>
|
||||
通过
|
||||
</Button>
|
||||
<Popconfirm title="确定要驳回吗?" okText="确定" cancelText="取消">
|
||||
<Button type="link" size="small" danger icon={<CloseOutlined />}>
|
||||
驳回
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return () => (
|
||||
<div class={styles.container}>
|
||||
<Card class={styles.filterCard}>
|
||||
<div class={styles.filterRow}>
|
||||
<Form ref={formRef} layout="inline">
|
||||
<Form.Item name="venueName">
|
||||
<Input
|
||||
value={formState.venueName}
|
||||
onChange={(e: any) => {
|
||||
formState.venueName = e.target.value;
|
||||
}}
|
||||
placeholder="请输入场馆名称"
|
||||
prefix={<SearchOutlined />}
|
||||
style={{ width: '200px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="收录状态" name="status">
|
||||
<Select
|
||||
value={formState.status}
|
||||
onChange={(val: any) => {
|
||||
formState.status = val;
|
||||
}}
|
||||
placeholder="请选择状态"
|
||||
style={{ width: '140px' }}
|
||||
allowClear={false}
|
||||
>
|
||||
{Object.entries(statusMap).map(([value, { text }]) => (
|
||||
<Select.Option key={value} value={value}>
|
||||
{text}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary">查询</Button>
|
||||
<Button onClick={handleReset}>重置</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class={styles.tableCard}>
|
||||
<div class={styles.tableBody}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData.value}
|
||||
pagination={false}
|
||||
scroll={{ x: 1000 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={styles.tableFooter}>
|
||||
<Pagination
|
||||
current={currentPage.value}
|
||||
onChange={(page: number) => {
|
||||
currentPage.value = page;
|
||||
}}
|
||||
total={500}
|
||||
showSizeChanger
|
||||
showQuickJumper
|
||||
showTotal={(total: number) => `共 ${total} 条`}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './useVenueRecordsModel';
|
||||
export * from './services';
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import { get } from '@/utils/request';
|
||||
|
||||
/** 收录记录行数据 */
|
||||
export interface VenueRecordItem {
|
||||
key: string;
|
||||
id: string;
|
||||
venueName: string;
|
||||
source: string;
|
||||
status: string;
|
||||
recordTime: string;
|
||||
}
|
||||
|
||||
/** 收录状态映射:状态值 -> 颜色 / 文案 */
|
||||
export const statusMap: Record<string, { color: string; text: string }> = {
|
||||
recorded: { color: 'green', text: '已收录' },
|
||||
reviewing: { color: 'orange', text: '审核中' },
|
||||
rejected: { color: 'red', text: '已驳回' },
|
||||
};
|
||||
|
||||
/** 收录记录查询参数 */
|
||||
export interface VenueRecordParams {
|
||||
page: number;
|
||||
size: number;
|
||||
venueName?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
const venueRecordApi = '/venue/records';
|
||||
|
||||
/**
|
||||
* 收录记录接口
|
||||
* @description 后端接口就绪后,在 useVenueRecordsModel 中启用替换 mock 数据
|
||||
*/
|
||||
export const getVenueRecords = (params: VenueRecordParams) => {
|
||||
return get(venueRecordApi, params);
|
||||
};
|
||||
|
||||
/** 临时 mock 数据(接口对接前占位展示) */
|
||||
export const mockVenueRecords: VenueRecordItem[] = [
|
||||
{
|
||||
key: '1',
|
||||
id: 'R001',
|
||||
venueName: '健身中心',
|
||||
source: '用户提交',
|
||||
status: 'recorded',
|
||||
recordTime: '2024-01-15 10:30:00',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
id: 'R002',
|
||||
venueName: '篮球公园',
|
||||
source: '平台采集',
|
||||
status: 'reviewing',
|
||||
recordTime: '2024-01-16 14:20:00',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
id: 'R003',
|
||||
venueName: '游泳馆',
|
||||
source: '用户提交',
|
||||
status: 'rejected',
|
||||
recordTime: '2024-01-17 09:15:00',
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { reactive, ref } from 'vue';
|
||||
import { getVenueRecords, mockVenueRecords, statusMap, type VenueRecordItem } from './services';
|
||||
|
||||
/** 筛选表单字段 */
|
||||
export interface VenueRecordFilterForm {
|
||||
venueName: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收录记录页面业务逻辑
|
||||
*/
|
||||
export function useVenueRecordsModel() {
|
||||
// 表格数据(暂用 mock,接口就绪后由 fetchList 填充)
|
||||
const tableData = ref<VenueRecordItem[]>(mockVenueRecords);
|
||||
// 当前页码
|
||||
const currentPage = ref(1);
|
||||
// 筛选表单实例
|
||||
const formRef = ref();
|
||||
// 筛选表单数据
|
||||
const formState = reactive<VenueRecordFilterForm>({
|
||||
venueName: '',
|
||||
status: undefined,
|
||||
});
|
||||
|
||||
/** 重置筛选表单 */
|
||||
const handleReset = () => {
|
||||
formRef.value?.resetFields();
|
||||
Object.assign(formState, { venueName: '', status: undefined });
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询收录记录
|
||||
* @description 后端接口就绪后启用,替换默认的 mock 数据
|
||||
*/
|
||||
const fetchList = async () => {
|
||||
const res: any = await getVenueRecords({
|
||||
page: currentPage.value,
|
||||
size: 10,
|
||||
...formState,
|
||||
});
|
||||
tableData.value = res?.data?.list ?? [];
|
||||
};
|
||||
|
||||
return {
|
||||
tableData,
|
||||
currentPage,
|
||||
formRef,
|
||||
formState,
|
||||
statusMap,
|
||||
handleReset,
|
||||
fetchList,
|
||||
};
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ export default defineConfig(({ mode }) => {
|
|||
},
|
||||
},
|
||||
build: {
|
||||
target: 'es2020',
|
||||
outDir: 'dist',
|
||||
sourcemap: mode !== 'production',
|
||||
chunkSizeWarningLimit: 1500,
|
||||
|
|
@ -50,5 +51,10 @@ export default defineConfig(({ mode }) => {
|
|||
},
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
target: 'es2020',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue