Compare commits
2 Commits
6f37264047
...
15b85dd9c1
| Author | SHA1 | Date | |
|---|---|---|---|
| 15b85dd9c1 | |||
| 5c7b605436 |
@@ -4,3 +4,4 @@ public
|
|||||||
*.config.js
|
*.config.js
|
||||||
*.config.ts
|
*.config.ts
|
||||||
start-debug.js
|
start-debug.js
|
||||||
|
src/assets/iconfont
|
||||||
|
|||||||
Vendored
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"[less]": {
|
"[less]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "vscode.css-language-features"
|
||||||
},
|
},
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
"*.vue": "vue"
|
"*.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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
};
|
||||||
Generated
+127
-18
@@ -1,14 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "cpms_web_admin_pc",
|
"name": "cpms_operation_platform",
|
||||||
"version": "1.0.0",
|
"version": "0.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "cpms_web_admin_pc",
|
"name": "cpms_operation_platform",
|
||||||
"version": "1.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons-vue": "^7.0.1",
|
"@ant-design/icons-vue": "^7.0.1",
|
||||||
|
"@yp-component/root": "^0.0.46",
|
||||||
"ant-design-vue": "^4.0.0",
|
"ant-design-vue": "^4.0.0",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
@@ -21,20 +22,21 @@
|
|||||||
"@commitlint/config-conventional": "^20.4.3",
|
"@commitlint/config-conventional": "^20.4.3",
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^20.10.0",
|
||||||
"@types/qs": "^6.15.0",
|
"@types/qs": "^6.15.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
"@vitejs/plugin-vue": "^4.5.0",
|
"@vitejs/plugin-vue": "^4.5.0",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
||||||
"eslint": "^8.0.0",
|
"eslint": "^8.57.1",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"eslint-plugin-vue": "^9.0.0",
|
"eslint-plugin-vue": "^9.33.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"less": "^4.6.4",
|
"less": "^4.6.4",
|
||||||
"lint-staged": "^15.0.0",
|
"lint-staged": "^15.0.0",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"typescript": "^5.3.0",
|
"typescript": "^5.3.0",
|
||||||
"vite": "^5.0.0",
|
"vite": "^5.0.0",
|
||||||
|
"vue-eslint-parser": "^10.4.1",
|
||||||
"vue-tsc": "^2.0.0"
|
"vue-tsc": "^2.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -984,6 +986,13 @@
|
|||||||
"url": "https://ko-fi.com/dangreen"
|
"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": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -1014,6 +1023,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "6.21.0",
|
"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,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1048,6 +1059,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
|
"resolved": "http://npm.nps.yunvip123.cn/@typescript-eslint/parser/-/parser-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1427,6 +1440,15 @@
|
|||||||
"version": "3.5.39",
|
"version": "3.5.39",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/accepts": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -2034,6 +2056,12 @@
|
|||||||
"typescript": ">=5"
|
"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": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -2699,6 +2727,9 @@
|
|||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "8.57.1",
|
"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,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2793,6 +2824,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-vue": {
|
"node_modules/eslint-plugin-vue": {
|
||||||
"version": "9.33.0",
|
"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,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2812,6 +2845,31 @@
|
|||||||
"eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0"
|
"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": {
|
"node_modules/eslint-scope": {
|
||||||
"version": "7.2.2",
|
"version": "7.2.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -5748,26 +5806,77 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-eslint-parser": {
|
"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,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.4.0",
|
||||||
"eslint-scope": "^7.1.1",
|
"eslint-scope": "^8.2.0 || ^9.0.0",
|
||||||
"eslint-visitor-keys": "^3.3.0",
|
"eslint-visitor-keys": "^4.2.0 || ^5.0.0",
|
||||||
"espree": "^9.3.1",
|
"espree": "^10.3.0 || ^11.0.0",
|
||||||
"esquery": "^1.4.0",
|
"esquery": "^1.6.0",
|
||||||
"lodash": "^4.17.21",
|
"semver": "^7.6.3"
|
||||||
"semver": "^7.3.6"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.17.0 || >=16.0.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/mysticatea"
|
"url": "https://github.com/sponsors/mysticatea"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"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": {
|
"node_modules/vue-router": {
|
||||||
|
|||||||
+5
-4
@@ -33,20 +33,21 @@
|
|||||||
"@commitlint/config-conventional": "^20.4.3",
|
"@commitlint/config-conventional": "^20.4.3",
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^20.10.0",
|
||||||
"@types/qs": "^6.15.0",
|
"@types/qs": "^6.15.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
"@vitejs/plugin-vue": "^4.5.0",
|
"@vitejs/plugin-vue": "^4.5.0",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
||||||
"eslint": "^8.0.0",
|
"eslint": "^8.57.1",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"eslint-plugin-vue": "^9.0.0",
|
"eslint-plugin-vue": "^9.33.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"less": "^4.6.4",
|
"less": "^4.6.4",
|
||||||
"lint-staged": "^15.0.0",
|
"lint-staged": "^15.0.0",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"typescript": "^5.3.0",
|
"typescript": "^5.3.0",
|
||||||
"vite": "^5.0.0",
|
"vite": "^5.0.0",
|
||||||
|
"vue-eslint-parser": "^10.4.1",
|
||||||
"vue-tsc": "^2.0.0"
|
"vue-tsc": "^2.0.0"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
|
|||||||
+11
-1
@@ -12,7 +12,17 @@ export default defineComponent({
|
|||||||
setup() {
|
setup() {
|
||||||
const theme = {
|
const theme = {
|
||||||
token: {
|
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 {
|
::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.match-ellipsis-single {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-layout .ant-layout-header {
|
||||||
|
padding-inline: 15px;
|
||||||
|
}
|
||||||
|
|||||||
+72
-19
@@ -1,46 +1,99 @@
|
|||||||
import { h } from 'vue';
|
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
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';
|
import { useBreadcrumb } from './useBreadcrumb';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础布局管理 Hook
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* 管理页面布局的核心逻辑。菜单、面包屑均由「路由表」派生,路由是唯一数据源:
|
||||||
|
* - 侧边栏菜单:generateMenu 根据路由的 meta.title / meta.icon 生成;
|
||||||
|
* - 菜单高亮 / 展开:根据当前路由匹配;
|
||||||
|
* - 面包屑:见 useBreadcrumb,取自 route.matched。
|
||||||
|
*/
|
||||||
export function useBasicLayout() {
|
export function useBasicLayout() {
|
||||||
|
// 路由实例,用于页面跳转
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
// 当前路由信息
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
// 获取面包屑数据
|
||||||
const { breadcrumbItems } = useBreadcrumb();
|
const { breadcrumbItems } = useBreadcrumb();
|
||||||
|
|
||||||
const menuItems = [
|
/**
|
||||||
{
|
* 侧边栏菜单配置
|
||||||
key: '/events',
|
* 由主布局("/")的子路由生成,不再手写维护
|
||||||
icon: h(TrophyOutlined),
|
*/
|
||||||
label: '我的赛事',
|
const layoutChildren = routes.find((r) => r.path === '/' && r.children?.length)?.children ?? [];
|
||||||
},
|
const menuItems = generateMenu(layoutChildren);
|
||||||
// { TODO 本期先做隐藏
|
|
||||||
// key: '/wallet',
|
|
||||||
// icon: h(WalletOutlined),
|
|
||||||
// label: '钱包管理',
|
|
||||||
// },
|
|
||||||
];
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收集所有叶子菜单(可点击、可高亮的具体菜单项)的 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 getActiveMenuKey = () => {
|
||||||
const matchedRoutes = route.matched;
|
if (leafMenuKeys.has(route.path)) {
|
||||||
for (const r of matchedRoutes) {
|
return route.path;
|
||||||
|
}
|
||||||
|
for (const r of route.matched) {
|
||||||
if (r.meta?.activeMenu) {
|
if (r.meta?.activeMenu) {
|
||||||
return r.meta.activeMenu as string;
|
return r.meta.activeMenu as string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (route.path.startsWith('/events')) return '/events';
|
|
||||||
if (route.path.startsWith('/wallet')) return '/wallet';
|
|
||||||
return route.path;
|
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));
|
router.push(String(info.key));
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
// 面包屑数据
|
||||||
breadcrumbItems,
|
breadcrumbItems,
|
||||||
|
// 菜单配置(由路由生成)
|
||||||
menuItems,
|
menuItems,
|
||||||
|
// 获取激活菜单 key
|
||||||
getActiveMenuKey,
|
getActiveMenuKey,
|
||||||
|
// 获取展开菜单 keys
|
||||||
|
getOpenKeys,
|
||||||
|
// 菜单点击处理
|
||||||
handleMenuClick,
|
handleMenuClick,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-18
@@ -1,28 +1,39 @@
|
|||||||
import { computed, h } from 'vue';
|
import { computed, h } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 面包屑管理 Hook
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* 面包屑完全由当前路由派生:遍历 route.matched(命中的路由链,从父到子),
|
||||||
|
* 取每一层 meta.title 作为面包屑标题。层级关系写在路由表里(分组路由 + 子路由),
|
||||||
|
* 不再维护额外的菜单/映射表。
|
||||||
|
*
|
||||||
|
* @returns breadcrumbs - 原始面包屑数据
|
||||||
|
* @returns breadcrumbItems - 处理后的面包屑项(非末项渲染为可点击链接)
|
||||||
|
*/
|
||||||
export function useBreadcrumb() {
|
export function useBreadcrumb() {
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据当前路径和记录路径,计算拼接后的路径
|
* 根据父级已拼接路径与当前路由记录的 path,计算该层的完整跳转路径
|
||||||
|
* 兼容子路由使用相对路径或绝对路径两种写法
|
||||||
*/
|
*/
|
||||||
const buildPath = (currentPath: string, recordPath: string): string => {
|
const buildPath = (parentPath: string, recordPath: string): string => {
|
||||||
if (recordPath.startsWith('/')) {
|
if (recordPath.startsWith('/')) {
|
||||||
return recordPath;
|
return recordPath;
|
||||||
}
|
}
|
||||||
return currentPath ? `${currentPath}/${recordPath}`.replace(/\/+/g, '/') : `/${recordPath}`;
|
return `${parentPath}/${recordPath}`.replace(/\/+/g, '/');
|
||||||
};
|
};
|
||||||
|
|
||||||
const breadcrumbs = computed(() => {
|
const breadcrumbs = computed(() => {
|
||||||
const matched = route.matched;
|
const items: Array<{ title: string; path: string }> = [];
|
||||||
const items: Array<{ title: string; path?: string }> = [];
|
|
||||||
let currentPath = '';
|
let currentPath = '';
|
||||||
|
|
||||||
for (const record of matched) {
|
for (const record of route.matched) {
|
||||||
currentPath = buildPath(currentPath, record.path);
|
currentPath = buildPath(currentPath, record.path);
|
||||||
if (record.meta && record.meta.title) {
|
if (record.meta?.title) {
|
||||||
items.push({
|
items.push({
|
||||||
title: record.meta.title as string,
|
title: record.meta.title as string,
|
||||||
path: currentPath,
|
path: currentPath,
|
||||||
@@ -38,25 +49,19 @@ export function useBreadcrumb() {
|
|||||||
key: index,
|
key: index,
|
||||||
title:
|
title:
|
||||||
item.path && index !== breadcrumbs.value.length - 1
|
item.path && index !== breadcrumbs.value.length - 1
|
||||||
? h(
|
? // 非末项:渲染为可点击链接
|
||||||
|
h(
|
||||||
'a',
|
'a',
|
||||||
{
|
{
|
||||||
onClick: (e: Event) => {
|
onClick: (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// 最保守方案:只有当前路径包含 /bracket/ 且目标路径是 /events/bracket 时才保留 query
|
router.push(item.path);
|
||||||
// 也就是只有:/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 : {},
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
item.title,
|
item.title,
|
||||||
)
|
)
|
||||||
: item.title,
|
: // 末项:仅显示文本,表示当前所在位置
|
||||||
|
item.title,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,57 +1,200 @@
|
|||||||
.container {
|
.container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
background: #f0f2f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sider {
|
.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 {
|
.logo {
|
||||||
height: 56px;
|
height: 64px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
padding: 0 24px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: #fff;
|
color: #262626;
|
||||||
background: rgba(255, 255, 255, 0.08);
|
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 {
|
.logoText {
|
||||||
font-size: 16px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
color: #262626;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
background: #fff;
|
background: #ffffff !important;
|
||||||
padding: 0 16px;
|
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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.trigger {
|
.trigger {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.3s;
|
transition: color 0.3s;
|
||||||
padding: 0 12px;
|
padding: 8px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
color: #595959;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #1677ff;
|
color: #1890ff;
|
||||||
|
background: #f5f5f5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.refreshIcon {
|
||||||
margin: 16px;
|
font-size: 16px;
|
||||||
padding: 24px;
|
cursor: pointer;
|
||||||
background: #fff;
|
padding: 8px;
|
||||||
border-radius: 8px;
|
display: inline-flex;
|
||||||
min-height: 280px;
|
align-items: center;
|
||||||
|
color: #595959;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.breadcrumb {
|
||||||
text-align: center;
|
margin-left: 12px;
|
||||||
color: rgba(0, 0, 0, 0.45);
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
+132
-30
@@ -1,69 +1,171 @@
|
|||||||
import { computed, defineComponent, ref } from 'vue';
|
import { computed, defineComponent, ref } from 'vue';
|
||||||
import { useRouter, useRoute, RouterView } from 'vue-router';
|
import { Layout, Menu, Breadcrumb, Modal, Avatar, Space } from 'ant-design-vue';
|
||||||
import { Layout, Menu } from 'ant-design-vue';
|
import { RouterView, useRouter } from 'vue-router';
|
||||||
import type { MenuProps } from 'ant-design-vue';
|
|
||||||
import {
|
import {
|
||||||
DashboardOutlined,
|
|
||||||
InfoCircleOutlined,
|
|
||||||
MenuFoldOutlined,
|
MenuFoldOutlined,
|
||||||
MenuUnfoldOutlined,
|
MenuUnfoldOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
SearchOutlined,
|
||||||
|
BellOutlined,
|
||||||
|
FullscreenOutlined,
|
||||||
|
UserOutlined,
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
|
import { useBasicLayout } from '../hooks/useBasicLayout';
|
||||||
|
import { auth } from '@/hooks/useAuth';
|
||||||
import styles from './BasicLayout.module.less';
|
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({
|
export default defineComponent({
|
||||||
name: 'BasicLayout',
|
name: 'BasicLayout',
|
||||||
setup() {
|
setup() {
|
||||||
const route = useRoute();
|
// 使用布局 Hook 获取菜单和面包屑数据
|
||||||
|
const { breadcrumbItems, menuItems, getActiveMenuKey, getOpenKeys, handleMenuClick } =
|
||||||
|
useBasicLayout();
|
||||||
|
|
||||||
|
// 路由实例,用于退出登录后跳转到登录页
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 侧边栏折叠状态
|
||||||
|
* true: 折叠状态(只显示图标)
|
||||||
|
* false: 展开状态(显示图标+文字)
|
||||||
|
*/
|
||||||
const collapsed = ref(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 = () => {
|
const toggleCollapsed = () => {
|
||||||
collapsed.value = !collapsed.value;
|
collapsed.value = !collapsed.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录:二次确认后跳转到登录页(不调用接口)
|
||||||
|
* - 点击用户名弹出确认弹框
|
||||||
|
* - 确定 -> 跳转登录页
|
||||||
|
* - 取消 -> 关闭弹框,不做任何处理
|
||||||
|
*/
|
||||||
|
const handleLogout = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认退出',
|
||||||
|
content: '确定要退出当前账号吗?',
|
||||||
|
okText: '确定',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: () => {
|
||||||
|
// 清除本地登录态(不调用接口),否则路由守卫会把 /login 重定向回首页
|
||||||
|
auth.logout();
|
||||||
|
router.replace('/login');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return () => (
|
return () => (
|
||||||
<Layout class={styles.container}>
|
<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}>
|
<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>
|
</div>
|
||||||
|
|
||||||
|
{/* 导航菜单 */}
|
||||||
<Menu
|
<Menu
|
||||||
theme="dark"
|
theme="light"
|
||||||
mode="inline"
|
mode="inline"
|
||||||
selectedKeys={selectedKeys.value}
|
selectedKeys={[getActiveMenuKey()]}
|
||||||
|
openKeys={getOpenKeys()}
|
||||||
items={menuItems}
|
items={menuItems}
|
||||||
onClick={handleMenuClick}
|
onClick={handleMenuClick}
|
||||||
/>
|
/>
|
||||||
</Sider>
|
</Sider>
|
||||||
|
|
||||||
|
{/* 右侧内容区域 */}
|
||||||
<Layout>
|
<Layout>
|
||||||
<Header class={styles.header}>
|
{/* 顶部 Header */}
|
||||||
<span class={styles.trigger} onClick={toggleCollapsed}>
|
<Header class={styles.header} style={{ background: '#ffffff' }}>
|
||||||
{collapsed.value ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
<div class={styles.headerLeft}>
|
||||||
</span>
|
{/* 折叠/展开按钮 */}
|
||||||
|
<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>
|
</Header>
|
||||||
|
|
||||||
|
{/* 内容区域 */}
|
||||||
<Content class={styles.content}>
|
<Content class={styles.content}>
|
||||||
<RouterView />
|
<div class={styles.pageContent}>
|
||||||
|
{/* 路由视图:根据当前路由渲染对应页面 */}
|
||||||
|
<RouterView />
|
||||||
|
</div>
|
||||||
</Content>
|
</Content>
|
||||||
|
|
||||||
<Footer class={styles.footer}>CPMS 运营平台 ©{new Date().getFullYear()}</Footer>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'ant-design-vue/dist/reset.css';
|
|||||||
import App from './App';
|
import App from './App';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
import './assets/styles/index.less';
|
import './assets/styles/index.less';
|
||||||
|
import './assets/iconfont/iconfont.css';
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||||
import { routes } from './routes';
|
import { routes } from './routes';
|
||||||
import { auth } from '@/hooks/useAuth';
|
import { auth } from '@/hooks/useAuth';
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.VITE_BASE_URL || '/'),
|
history: createWebHashHistory(import.meta.env.VITE_BASE_URL || '/'),
|
||||||
routes,
|
routes,
|
||||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||||
});
|
});
|
||||||
|
|||||||
+149
-34
@@ -1,40 +1,23 @@
|
|||||||
import type { RouteRecordRaw } from 'vue-router';
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
import {
|
||||||
|
BarChartOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
StarOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
NotificationOutlined,
|
||||||
|
BorderTopOutlined,
|
||||||
|
BoxPlotOutlined,
|
||||||
|
SafetyCertificateOutlined,
|
||||||
|
} from '@ant-design/icons-vue';
|
||||||
import { createRoute } from './utils';
|
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[] = [
|
export const routes: RouteRecordRaw[] = [
|
||||||
// ── 登录页(不经过布局)──
|
// ── 登录页(不经过布局)──
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'Login',
|
name: 'Login',
|
||||||
component: () => import('@/pages/login'),
|
component: () => import('@/pages/login'),
|
||||||
meta: { title: '登录' },
|
meta: { title: '登录', hideInMenu: true },
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── 主布局(需要登录)──
|
// ── 主布局(需要登录)──
|
||||||
@@ -42,17 +25,149 @@ export const routes: RouteRecordRaw[] = [
|
|||||||
path: '/',
|
path: '/',
|
||||||
component: () => import('@/layouts/BasicLayout'),
|
component: () => import('@/layouts/BasicLayout'),
|
||||||
children: [
|
children: [
|
||||||
// 业务模块路由在此添加,例如:eventsRoutes, walletRoutes
|
// 数据分析
|
||||||
|
|
||||||
createRoute('/dashboard', () => import('@/views/Dashboard'), {
|
createRoute('/dashboard', () => import('@/views/Dashboard'), {
|
||||||
title: '工作台',
|
title: '数据分析',
|
||||||
icon: 'DashboardOutlined',
|
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'), {
|
createRoute('/about', () => import('@/views/About'), {
|
||||||
title: '关于',
|
title: '关于',
|
||||||
icon: 'InfoCircleOutlined',
|
hideInMenu: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 默认重定向
|
// 默认重定向
|
||||||
{ path: '/', redirect: '/dashboard' },
|
{ path: '/', redirect: '/dashboard' },
|
||||||
],
|
],
|
||||||
|
|||||||
+54
-5
@@ -1,20 +1,25 @@
|
|||||||
|
import { h, type Component, type VNode } from 'vue';
|
||||||
import type { RouteRecordRaw } from 'vue-router';
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
/** 路由 meta 类型 */
|
/** 路由 meta 类型 */
|
||||||
export type AppMeta = {
|
export type AppMeta = {
|
||||||
|
/** 页面 / 菜单标题,同时用于面包屑 */
|
||||||
title: string;
|
title: string;
|
||||||
icon?: string;
|
/**
|
||||||
|
* 菜单图标(仅一级菜单需要),支持两种写法:
|
||||||
|
* - iconfont 的 class 名,如 'icon-changguan1'
|
||||||
|
* - 组件,如 ant-design 图标 BarChartOutlined
|
||||||
|
*/
|
||||||
|
icon?: Component | string;
|
||||||
|
/** 是否在侧边栏菜单中隐藏(详情页等) */
|
||||||
hideInMenu?: boolean;
|
hideInMenu?: boolean;
|
||||||
|
/** 高亮的菜单 key(路径不等于菜单 key 的场景,如详情页) */
|
||||||
activeMenu?: string;
|
activeMenu?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 快速创建路由记录
|
* 快速创建路由记录
|
||||||
* 根据路径自动生成 PascalCase 路由 name
|
* 根据路径自动生成 PascalCase 路由 name
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* createRoute('list', () => import('@/pages/home'), { title: '赛事列表' })
|
|
||||||
* // => { path: 'list', name: 'List', meta: {...}, component: ... }
|
|
||||||
*/
|
*/
|
||||||
export function createRoute(
|
export function createRoute(
|
||||||
path: string,
|
path: string,
|
||||||
@@ -38,3 +43,47 @@ export function createRoute(
|
|||||||
children,
|
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;
|
||||||
|
}
|
||||||
|
|||||||
+2
-1
@@ -27,7 +27,8 @@ export interface SelectOption {
|
|||||||
declare module 'vue-router' {
|
declare module 'vue-router' {
|
||||||
interface RouteMeta {
|
interface RouteMeta {
|
||||||
title?: string;
|
title?: string;
|
||||||
icon?: string;
|
/** 菜单图标:iconfont 的 class 名(字符串) 或 组件(如 antd 图标) */
|
||||||
|
icon?: import('vue').Component | string;
|
||||||
/** 是否需要登录 */
|
/** 是否需要登录 */
|
||||||
requiresAuth?: boolean;
|
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: {
|
build: {
|
||||||
|
target: 'es2020',
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
sourcemap: mode !== 'production',
|
sourcemap: mode !== 'production',
|
||||||
chunkSizeWarningLimit: 1500,
|
chunkSizeWarningLimit: 1500,
|
||||||
@@ -50,5 +51,10 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
esbuildOptions: {
|
||||||
|
target: 'es2020',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user