feat: 添加前端项目基础架构和功能模块
refactor: 优化市场价值计算逻辑和行业均值计算 fix: 修复环境变量和配置文件问题 chore: 更新Docker镜像版本至v1.4 docs: 更新需求文档和部署说明 style: 调整代码格式和样式 build: 配置Vite构建工具和依赖管理 test: 添加前端组件测试基础 ci: 设置CI/CD脚本和工作流 perf: 优化前端性能和数据加载
@ -327,7 +327,7 @@ async def _extract_calculation_params_b1(data: UserValuationCreate) -> Dict[str,
|
|||||||
|
|
||||||
# 流量因子B12相关参数
|
# 流量因子B12相关参数
|
||||||
# 近30天搜索指数S1 - 从社交媒体数据计算 TODO 需要使用第三方API
|
# 近30天搜索指数S1 - 从社交媒体数据计算 TODO 需要使用第三方API
|
||||||
baidu_index = 0
|
baidu_index = 1
|
||||||
|
|
||||||
# 获取微信指数并计算近30天平均值
|
# 获取微信指数并计算近30天平均值
|
||||||
try:
|
try:
|
||||||
@ -336,9 +336,9 @@ async def _extract_calculation_params_b1(data: UserValuationCreate) -> Dict[str,
|
|||||||
logger.info(f"资产 '{data.asset_name}' 的微信指数近30天平均值: {wechat_index}")
|
logger.info(f"资产 '{data.asset_name}' 的微信指数近30天平均值: {wechat_index}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取微信指数失败: {e}")
|
logger.error(f"获取微信指数失败: {e}")
|
||||||
wechat_index = 0
|
wechat_index = 1
|
||||||
|
|
||||||
weibo_index = 0
|
weibo_index = 1
|
||||||
search_index_s1 = calculate_search_index_s1(baidu_index, wechat_index, weibo_index) # 默认值,实际应从API获取
|
search_index_s1 = calculate_search_index_s1(baidu_index, wechat_index, weibo_index) # 默认值,实际应从API获取
|
||||||
|
|
||||||
# 行业均值S2 - 从数据库查询行业数据计算
|
# 行业均值S2 - 从数据库查询行业数据计算
|
||||||
@ -433,6 +433,7 @@ async def _extract_calculation_params_b2(data: UserValuationCreate) -> Dict[str,
|
|||||||
# 纹样基因值B22相关参数
|
# 纹样基因值B22相关参数
|
||||||
|
|
||||||
# 以下三项需由后续模型/服务计算;此处提供默认可计算占位
|
# 以下三项需由后续模型/服务计算;此处提供默认可计算占位
|
||||||
|
#
|
||||||
# 历史传承度HI(用户填写)
|
# 历史传承度HI(用户填写)
|
||||||
historical_inheritance = sum([safe_float(i) for i in data.historical_evidence])
|
historical_inheritance = sum([safe_float(i) for i in data.historical_evidence])
|
||||||
structure_complexity = 1.5 # 默认值 纹样基因熵值B22(系统计算)
|
structure_complexity = 1.5 # 默认值 纹样基因熵值B22(系统计算)
|
||||||
|
|||||||
@ -113,12 +113,9 @@ class MarketValueCCalculator:
|
|||||||
return:
|
return:
|
||||||
Dict: 包含所有中间计算结果和最终结果的字典
|
Dict: 包含所有中间计算结果和最终结果的字典
|
||||||
"""
|
"""
|
||||||
# 获取动态默认值
|
|
||||||
default_price = await self._get_dynamic_default_price(input_data)
|
|
||||||
|
|
||||||
# 计算市场竞价C1
|
# 计算市场竞价C1
|
||||||
market_bidding_c1 = self.market_bidding_calculator.calculate_market_bidding_c1(
|
market_bidding_c1 = self.market_bidding_calculator.calculate_market_bidding_c1(
|
||||||
transaction_data={'weighted_average_price': input_data.get('weighted_average_price', default_price)},
|
transaction_data={'weighted_average_price': input_data.get('weighted_average_price', 0)},
|
||||||
manual_bids=input_data.get('manual_bids', []),
|
manual_bids=input_data.get('manual_bids', []),
|
||||||
expert_valuations=input_data.get('expert_valuations', [])
|
expert_valuations=input_data.get('expert_valuations', [])
|
||||||
)
|
)
|
||||||
|
|||||||
@ -23,7 +23,7 @@ class HeatCoefficientC2Calculator:
|
|||||||
"""
|
"""
|
||||||
# 计算浏览热度分
|
# 计算浏览热度分
|
||||||
browse_heat_score = self.calculate_browse_heat_score(daily_browse_volume, collection_count)
|
browse_heat_score = self.calculate_browse_heat_score(daily_browse_volume, collection_count)
|
||||||
|
print("浏览热度分: ")
|
||||||
|
|
||||||
heat_coefficient = 1 + browse_heat_score
|
heat_coefficient = 1 + browse_heat_score
|
||||||
|
|
||||||
|
|||||||
@ -70,7 +70,7 @@ if __name__ == "__main__":
|
|||||||
manual_bids = [950.0, 1000.0, 1050.0, 1100.0] # 用户填写
|
manual_bids = [950.0, 1000.0, 1050.0, 1100.0] # 用户填写
|
||||||
|
|
||||||
# 优先级3:专家估值
|
# 优先级3:专家估值
|
||||||
expert_valuations = [980.0, 1020.0, 990.0] # 系统配置
|
expert_valuations = [0.0, 0.0, 0.0] # 系统配置
|
||||||
|
|
||||||
# 计算市场竞价C1
|
# 计算市场竞价C1
|
||||||
market_bidding_c1 = calculator.calculate_market_bidding_c1(
|
market_bidding_c1 = calculator.calculate_market_bidding_c1(
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional, Dict, Any
|
from typing import Optional, Dict, Any
|
||||||
from app.models.industry import Industry
|
from app.models.industry import Industry
|
||||||
|
from app.models.index import Index
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -44,6 +46,7 @@ async def get_industry_data_by_name(industry_name: str) -> Optional[Dict[str, An
|
|||||||
|
|
||||||
|
|
||||||
async def calculate_industry_average_s2(industry_name: str) -> float:
|
async def calculate_industry_average_s2(industry_name: str) -> float:
|
||||||
|
# todo : 使用index 搜索的数据
|
||||||
"""
|
"""
|
||||||
计算行业均值S2
|
计算行业均值S2
|
||||||
|
|
||||||
@ -53,21 +56,13 @@ async def calculate_industry_average_s2(industry_name: str) -> float:
|
|||||||
Returns:
|
Returns:
|
||||||
行业均值S2,如果查询失败则返回0.0
|
行业均值S2,如果查询失败则返回0.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
industry_data = await get_industry_data_by_name(industry_name)
|
index_data = await Index.filter(name=industry_name).first()
|
||||||
if industry_data:
|
if index_data:
|
||||||
# S2 = ROE * 修正系数
|
# S2 = ROE * 修正系数
|
||||||
roe = industry_data.get('roe', 0.0)
|
logger.info(f"行业 {industry_name} S2计算: S2={index_data.search_num}")
|
||||||
fix_num = industry_data.get('fix_num', 0.0)
|
return index_data.search_num
|
||||||
s2_value = roe * fix_num
|
|
||||||
|
|
||||||
# 确保S2值为正数,避免对数计算错误
|
|
||||||
if s2_value <= 0:
|
|
||||||
logger.warning(f"行业 {industry_name} S2计算值为负数或零: ROE={roe}, 修正系数={fix_num}, S2={s2_value},使用默认值0.01")
|
|
||||||
s2_value = 0.01 # 使用小的正数避免对数计算错误
|
|
||||||
|
|
||||||
logger.info(f"行业 {industry_name} S2计算: ROE={roe}, 修正系数={fix_num}, S2={s2_value}")
|
|
||||||
return s2_value
|
|
||||||
else:
|
else:
|
||||||
logger.warning(f"未找到行业 {industry_name} 的数据,返回默认值0.01")
|
logger.warning(f"未找到行业 {industry_name} 的数据,返回默认值0.01")
|
||||||
return 0.01 # 返回小的正数而不是0.0
|
return 0.01 # 返回小的正数而不是0.0
|
||||||
|
|||||||
3
node_modules/.vite/deps_temp_e96670e1/package.json
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
8
web1/.env.development
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# 资源公共路径,需要以 /开头和结尾
|
||||||
|
VITE_PUBLIC_PATH = '/'
|
||||||
|
|
||||||
|
# 是否启用代理
|
||||||
|
VITE_USE_PROXY = true
|
||||||
|
|
||||||
|
# base api
|
||||||
|
VITE_BASE_API = 'https://value.cdcee.net/api/v1'
|
||||||
11
web1/.env.production
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# 资源公共路径,需要以 /开头和结尾
|
||||||
|
VITE_PUBLIC_PATH = '/'
|
||||||
|
|
||||||
|
# base api
|
||||||
|
VITE_BASE_API = 'https://value.cdcee.net/api/v1'
|
||||||
|
|
||||||
|
# 是否启用压缩
|
||||||
|
VITE_USE_COMPRESS = true
|
||||||
|
|
||||||
|
# 压缩类型
|
||||||
|
VITE_COMPRESS_TYPE = gzip
|
||||||
62
web1/.eslint-global-variables.json
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"globals": {
|
||||||
|
"$loadingBar": true,
|
||||||
|
"$message": true,
|
||||||
|
"defineOptions": true,
|
||||||
|
"$dialog": true,
|
||||||
|
"$notification": true,
|
||||||
|
"EffectScope": true,
|
||||||
|
"computed": true,
|
||||||
|
"createApp": true,
|
||||||
|
"customRef": true,
|
||||||
|
"defineAsyncComponent": true,
|
||||||
|
"defineComponent": true,
|
||||||
|
"effectScope": true,
|
||||||
|
"getCurrentInstance": true,
|
||||||
|
"getCurrentScope": true,
|
||||||
|
"h": true,
|
||||||
|
"inject": true,
|
||||||
|
"isProxy": true,
|
||||||
|
"isReactive": true,
|
||||||
|
"isReadonly": true,
|
||||||
|
"isRef": true,
|
||||||
|
"markRaw": true,
|
||||||
|
"nextTick": true,
|
||||||
|
"onActivated": true,
|
||||||
|
"onBeforeMount": true,
|
||||||
|
"onBeforeUnmount": true,
|
||||||
|
"onBeforeUpdate": true,
|
||||||
|
"onDeactivated": true,
|
||||||
|
"onErrorCaptured": true,
|
||||||
|
"onMounted": true,
|
||||||
|
"onRenderTracked": true,
|
||||||
|
"onRenderTriggered": true,
|
||||||
|
"onScopeDispose": true,
|
||||||
|
"onServerPrefetch": true,
|
||||||
|
"onUnmounted": true,
|
||||||
|
"onUpdated": true,
|
||||||
|
"provide": true,
|
||||||
|
"reactive": true,
|
||||||
|
"readonly": true,
|
||||||
|
"ref": true,
|
||||||
|
"resolveComponent": true,
|
||||||
|
"shallowReactive": true,
|
||||||
|
"shallowReadonly": true,
|
||||||
|
"shallowRef": true,
|
||||||
|
"toRaw": true,
|
||||||
|
"toRef": true,
|
||||||
|
"toRefs": true,
|
||||||
|
"triggerRef": true,
|
||||||
|
"unref": true,
|
||||||
|
"useAttrs": true,
|
||||||
|
"useCssModule": true,
|
||||||
|
"useCssVars": true,
|
||||||
|
"useRoute": true,
|
||||||
|
"useRouter": true,
|
||||||
|
"useSlots": true,
|
||||||
|
"watch": true,
|
||||||
|
"watchEffect": true,
|
||||||
|
"watchPostEffect": true,
|
||||||
|
"watchSyncEffect": true
|
||||||
|
}
|
||||||
|
}
|
||||||
4
web1/.eslintignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
public
|
||||||
|
package.json
|
||||||
26
web1/.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
stats.html
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
3
web1/.prettierignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/node_modules/**
|
||||||
|
/dist/*
|
||||||
|
/public/*
|
||||||
6
web1/.prettierrc.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": false,
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
20
web1/README.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
## 快速开始
|
||||||
|
|
||||||
|
进入前端目录
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd web
|
||||||
|
```
|
||||||
|
|
||||||
|
安装依赖(建议使用pnpm: https://pnpm.io/zh/installation)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm i -g pnpm # 已安装可忽略
|
||||||
|
pnpm i # 或者 npm i
|
||||||
|
```
|
||||||
|
|
||||||
|
启动
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
13
web1/build/config/define.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* * 此处定义的是全局常量,启动或打包后将添加到window中
|
||||||
|
* https://vitejs.cn/config/#define
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 项目构建时间
|
||||||
|
const _BUILD_TIME_ = JSON.stringify(dayjs().format('YYYY-MM-DD HH:mm:ss'))
|
||||||
|
|
||||||
|
export const viteDefine = {
|
||||||
|
_BUILD_TIME_,
|
||||||
|
}
|
||||||
1
web1/build/config/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './define'
|
||||||
23
web1/build/constant.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export const OUTPUT_DIR = 'dist'
|
||||||
|
|
||||||
|
export const PROXY_CONFIG = {
|
||||||
|
// /**
|
||||||
|
// * @desc 替换匹配值
|
||||||
|
// * @请求路径 http://localhost:3100/api/user
|
||||||
|
// * @转发路径 http://localhost:9999/api/v1 +/user
|
||||||
|
// */
|
||||||
|
// '/api': {
|
||||||
|
// target: 'http://localhost:9999/api/v1',
|
||||||
|
// changeOrigin: true,
|
||||||
|
// rewrite: (path) => path.replace(new RegExp('^/api'), ''),
|
||||||
|
// },
|
||||||
|
/**
|
||||||
|
* @desc 不替换匹配值
|
||||||
|
* @请求路径 http://localhost:3100/api/v1/user
|
||||||
|
* @转发路径 http://localhost:9999/api/v1/user
|
||||||
|
*/
|
||||||
|
'/api/v1': {
|
||||||
|
target: 'http://124.222.245.240:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
15
web1/build/plugin/html.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { createHtmlPlugin } from 'vite-plugin-html'
|
||||||
|
|
||||||
|
export function configHtmlPlugin(viteEnv, isBuild) {
|
||||||
|
const { VITE_TITLE } = viteEnv
|
||||||
|
|
||||||
|
const htmlPlugin = createHtmlPlugin({
|
||||||
|
minify: isBuild,
|
||||||
|
inject: {
|
||||||
|
data: {
|
||||||
|
title: VITE_TITLE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return htmlPlugin
|
||||||
|
}
|
||||||
35
web1/build/plugin/index.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* * unocss插件,原子css
|
||||||
|
* https://github.com/antfu/unocss
|
||||||
|
*/
|
||||||
|
import Unocss from 'unocss/vite'
|
||||||
|
|
||||||
|
// rollup打包分析插件
|
||||||
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
|
// 压缩
|
||||||
|
import viteCompression from 'vite-plugin-compression'
|
||||||
|
|
||||||
|
import { configHtmlPlugin } from './html'
|
||||||
|
import unplugin from './unplugin'
|
||||||
|
|
||||||
|
export function createVitePlugins(viteEnv, isBuild) {
|
||||||
|
const plugins = [vue(), ...unplugin, configHtmlPlugin(viteEnv, isBuild), Unocss()]
|
||||||
|
|
||||||
|
if (viteEnv.VITE_USE_COMPRESS) {
|
||||||
|
plugins.push(viteCompression({ algorithm: viteEnv.VITE_COMPRESS_TYPE || 'gzip' }))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBuild) {
|
||||||
|
plugins.push(
|
||||||
|
visualizer({
|
||||||
|
open: true,
|
||||||
|
gzipSize: true,
|
||||||
|
brotliSize: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins
|
||||||
|
}
|
||||||
46
web1/build/plugin/unplugin.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { resolve } from 'path'
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||||
|
import { FileSystemIconLoader } from 'unplugin-icons/loaders'
|
||||||
|
import IconsResolver from 'unplugin-icons/resolver'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* * unplugin-icons插件,自动引入iconify图标
|
||||||
|
* usage: https://github.com/antfu/unplugin-icons
|
||||||
|
* 图标库: https://icones.js.org/
|
||||||
|
*/
|
||||||
|
import Icons from 'unplugin-icons/vite'
|
||||||
|
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
|
||||||
|
|
||||||
|
import { getSrcPath } from '../utils'
|
||||||
|
|
||||||
|
const customIconPath = resolve(getSrcPath(), 'assets/svg')
|
||||||
|
|
||||||
|
export default [
|
||||||
|
AutoImport({
|
||||||
|
imports: ['vue', 'vue-router'],
|
||||||
|
dts: false,
|
||||||
|
}),
|
||||||
|
Icons({
|
||||||
|
compiler: 'vue3',
|
||||||
|
customCollections: {
|
||||||
|
custom: FileSystemIconLoader(customIconPath),
|
||||||
|
},
|
||||||
|
scale: 1,
|
||||||
|
defaultClass: 'inline-block',
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
resolvers: [
|
||||||
|
NaiveUiResolver(),
|
||||||
|
IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' }),
|
||||||
|
],
|
||||||
|
dts: false,
|
||||||
|
}),
|
||||||
|
createSvgIconsPlugin({
|
||||||
|
iconDirs: [customIconPath],
|
||||||
|
symbolId: 'icon-custom-[dir]-[name]',
|
||||||
|
inject: 'body-last',
|
||||||
|
customDomId: '__CUSTOM_SVG_ICON__',
|
||||||
|
}),
|
||||||
|
]
|
||||||
15
web1/build/script/build-cname.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { resolve } from 'path'
|
||||||
|
import chalk from 'chalk'
|
||||||
|
import { writeFileSync } from 'fs-extra'
|
||||||
|
import { OUTPUT_DIR } from '../constant'
|
||||||
|
import { getEnvConfig, getRootPath } from '../utils'
|
||||||
|
|
||||||
|
export function runBuildCNAME() {
|
||||||
|
const { VITE_CNAME } = getEnvConfig()
|
||||||
|
if (!VITE_CNAME) return
|
||||||
|
try {
|
||||||
|
writeFileSync(resolve(getRootPath(), `${OUTPUT_DIR}/CNAME`), VITE_CNAME)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(chalk.red('CNAME file failed to package:\n' + error))
|
||||||
|
}
|
||||||
|
}
|
||||||
14
web1/build/script/index.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import chalk from 'chalk'
|
||||||
|
import { runBuildCNAME } from './build-cname'
|
||||||
|
|
||||||
|
export const runBuild = async () => {
|
||||||
|
try {
|
||||||
|
runBuildCNAME()
|
||||||
|
console.log(`✨ ${chalk.cyan('build successfully!')}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(chalk.red('vite build error:\n' + error))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runBuild()
|
||||||
70
web1/build/utils.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* * 项目根路径
|
||||||
|
* @descrition 结尾不带/
|
||||||
|
*/
|
||||||
|
export function getRootPath() {
|
||||||
|
return path.resolve(process.cwd())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* * 项目src路径
|
||||||
|
* @param srcName src目录名称(默认: "src")
|
||||||
|
* @descrition 结尾不带斜杠
|
||||||
|
*/
|
||||||
|
export function getSrcPath(srcName = 'src') {
|
||||||
|
return path.resolve(getRootPath(), srcName)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertEnv(envOptions) {
|
||||||
|
const result = {}
|
||||||
|
if (!envOptions) return result
|
||||||
|
|
||||||
|
for (const envKey in envOptions) {
|
||||||
|
let envVal = envOptions[envKey]
|
||||||
|
if (['true', 'false'].includes(envVal)) envVal = envVal === 'true'
|
||||||
|
|
||||||
|
if (['VITE_PORT'].includes(envKey)) envVal = +envVal
|
||||||
|
|
||||||
|
result[envKey] = envVal
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前环境下生效的配置文件名
|
||||||
|
*/
|
||||||
|
function getConfFiles() {
|
||||||
|
const script = process.env.npm_lifecycle_script
|
||||||
|
const reg = new RegExp('--mode ([a-z_\\d]+)')
|
||||||
|
const result = reg.exec(script)
|
||||||
|
if (result) {
|
||||||
|
const mode = result[1]
|
||||||
|
return ['.env', '.env.local', `.env.${mode}`]
|
||||||
|
}
|
||||||
|
return ['.env', '.env.local', '.env.production']
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEnvConfig(match = 'VITE_', confFiles = getConfFiles()) {
|
||||||
|
let envConfig = {}
|
||||||
|
confFiles.forEach((item) => {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(path.resolve(process.cwd(), item))) {
|
||||||
|
const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item)))
|
||||||
|
envConfig = { ...envConfig, ...env }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error in parsing ${item}`, e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const reg = new RegExp(`^(${match})`)
|
||||||
|
Object.keys(envConfig).forEach((key) => {
|
||||||
|
if (!reg.test(key)) {
|
||||||
|
Reflect.deleteProperty(envConfig, key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return envConfig
|
||||||
|
}
|
||||||
16
web1/i18n/index.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
import { lStorage } from '@/utils'
|
||||||
|
|
||||||
|
import messages from './messages'
|
||||||
|
|
||||||
|
const currentLocale = lStorage.get('locale')
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
legacy: false,
|
||||||
|
globalInjection: true,
|
||||||
|
locale: currentLocale || 'cn',
|
||||||
|
fallbackLocale: 'cn',
|
||||||
|
messages: messages,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default i18n
|
||||||
62
web1/i18n/messages/cn.json
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"lang": "中文",
|
||||||
|
"app_name": "Vue FastAPI Admin",
|
||||||
|
"header": {
|
||||||
|
"label_profile": "个人信息",
|
||||||
|
"label_logout": "退出登录",
|
||||||
|
"label_logout_dialog_title": "提示",
|
||||||
|
"text_logout_confirm": "确认退出?",
|
||||||
|
"text_logout_success": "已退出登录"
|
||||||
|
},
|
||||||
|
"views": {
|
||||||
|
"login": {
|
||||||
|
"text_login": "登录",
|
||||||
|
"message_input_username_password": "请输入用户名和密码",
|
||||||
|
"message_verifying": "正在验证...",
|
||||||
|
"message_login_success": "登录成功"
|
||||||
|
},
|
||||||
|
"workbench": {
|
||||||
|
"label_workbench": "工作台",
|
||||||
|
"text_hello": "hello, {username}",
|
||||||
|
"text_welcome": "今天又是元气满满的一天!",
|
||||||
|
"label_number_of_items": "项目数",
|
||||||
|
"label_upcoming": "待办",
|
||||||
|
"label_information": "消息",
|
||||||
|
"label_project": "项目",
|
||||||
|
"label_more": "更多"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"label_profile": "个人中心",
|
||||||
|
"label_modify_information": "修改信息",
|
||||||
|
"label_change_password": "修改密码",
|
||||||
|
"label_avatar": "头像",
|
||||||
|
"label_username": "用户姓名",
|
||||||
|
"label_email": "邮箱",
|
||||||
|
"label_old_password": "旧密码",
|
||||||
|
"label_new_password": "新密码",
|
||||||
|
"label_confirm_password": "确认密码",
|
||||||
|
"placeholder_username": "请填写姓名",
|
||||||
|
"placeholder_email": "请填写邮箱",
|
||||||
|
"placeholder_old_password": "请输入旧密码",
|
||||||
|
"placeholder_new_password": "请输入新密码",
|
||||||
|
"placeholder_confirm_password": "请再次输入新密码",
|
||||||
|
"message_username_required": "请输入昵称",
|
||||||
|
"message_old_password_required": "请输入旧密码",
|
||||||
|
"message_new_password_required": "请输入新密码",
|
||||||
|
"message_password_confirmation_required": "请再次输入密码",
|
||||||
|
"message_password_confirmation_diff": "两次密码输入不一致"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"label_error": "错误页",
|
||||||
|
"text_back_to_home": "返回首页"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"text": {
|
||||||
|
"update_success": "修改成功"
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"update": "修改"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
62
web1/i18n/messages/en.json
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"lang": "English",
|
||||||
|
"app_name": "Vue FastAPI Admin",
|
||||||
|
"header": {
|
||||||
|
"label_profile": "Profile",
|
||||||
|
"label_logout": "Logout",
|
||||||
|
"label_logout_dialog_title": "Hint",
|
||||||
|
"text_logout_confirm": "Logout confirm",
|
||||||
|
"text_logout_success": "Logout success"
|
||||||
|
},
|
||||||
|
"views": {
|
||||||
|
"login": {
|
||||||
|
"text_login": "Login",
|
||||||
|
"message_input_username_password": "Please enter username and password",
|
||||||
|
"message_verifying": "Verifying...",
|
||||||
|
"message_login_success": "Login successful"
|
||||||
|
},
|
||||||
|
"workbench": {
|
||||||
|
"label_workbench": "Workbench",
|
||||||
|
"text_hello": "hello, {username}",
|
||||||
|
"text_welcome": "Today is another day full of energy!",
|
||||||
|
"label_number_of_items": "Number of items",
|
||||||
|
"label_upcoming": "Upcoming",
|
||||||
|
"label_information": "Information",
|
||||||
|
"label_project": "Project",
|
||||||
|
"label_more": "More"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"label_profile": "Profile",
|
||||||
|
"label_modify_information": "Modify your information",
|
||||||
|
"label_change_password": "Change password",
|
||||||
|
"label_avatar": "Avatar",
|
||||||
|
"label_username": "Username",
|
||||||
|
"label_email": "Email",
|
||||||
|
"label_old_password": "Old password",
|
||||||
|
"label_new_password": "New password",
|
||||||
|
"label_confirm_password": "Password confirmation",
|
||||||
|
"placeholder_username": "Please fill in your name",
|
||||||
|
"placeholder_email": "Please fill in your email address",
|
||||||
|
"placeholder_old_password": "Please enter the old password",
|
||||||
|
"placeholder_new_password": "Please enter a new password",
|
||||||
|
"placeholder_confirm_password": "Please enter the confirm password",
|
||||||
|
"message_username_required": "Please enter username",
|
||||||
|
"message_old_password_required": "Please enter the old password",
|
||||||
|
"message_new_password_required": "Please enter a new password",
|
||||||
|
"message_password_confirmation_required": "Please enter confirm password",
|
||||||
|
"message_password_confirmation_diff": "Two password inputs are inconsistent"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"label_error": "Error",
|
||||||
|
"text_back_to_home": "Back to home"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"text": {
|
||||||
|
"update_success": "Update success"
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"update": "Update"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
web1/i18n/messages/index.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import * as en from './en.json'
|
||||||
|
import * as cn from './cn.json'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
en,
|
||||||
|
cn,
|
||||||
|
}
|
||||||
35
web1/index.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="cn">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<meta http-equiv="Pragma" content="no-cache" />
|
||||||
|
<meta http-equiv="Cache-control" content="no-cache" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" href="/favicon.svg" />
|
||||||
|
<link rel="stylesheet" href="/resource/loading.css" />
|
||||||
|
|
||||||
|
<title><%= title %></title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<!-- 白屏时的loading效果 -->
|
||||||
|
<div class="loading-container">
|
||||||
|
<div id="loadingLogo" class="loading-svg"></div>
|
||||||
|
<div class="loading-spin__container">
|
||||||
|
<div class="loading-spin">
|
||||||
|
<div class="left-0 top-0 loading-spin-item"></div>
|
||||||
|
<div class="left-0 bottom-0 loading-spin-item loading-delay-500"></div>
|
||||||
|
<div class="right-0 top-0 loading-spin-item loading-delay-1000"></div>
|
||||||
|
<div class="right-0 bottom-0 loading-spin-item loading-delay-1500"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="loading-title"><%= title %></div> -->
|
||||||
|
</div>
|
||||||
|
<script src="/resource/loading.js"></script>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
14
web1/jsconfig.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./*"],
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
},
|
||||||
|
"jsx": "preserve",
|
||||||
|
"allowJs": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
9149
web1/package-lock.json
generated
Normal file
60
web1/package.json
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"name": "vue-fastapi-admin-web",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint --ext .js,.vue .",
|
||||||
|
"lint:fix": "eslint --fix --ext .js,.vue .",
|
||||||
|
"lint:staged": "lint-staged",
|
||||||
|
"prettier": "npx prettier --write ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@iconify/json": "^2.2.228",
|
||||||
|
"@iconify/vue": "^4.1.1",
|
||||||
|
"@unocss/eslint-config": "^0.55.0",
|
||||||
|
"@vicons/ionicons5": "^0.13.0",
|
||||||
|
"@vueuse/core": "^10.3.0",
|
||||||
|
"@zclzone/eslint-config": "^0.0.4",
|
||||||
|
"axios": "^1.4.0",
|
||||||
|
"dayjs": "^1.11.9",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"echarts": "^5.4.3",
|
||||||
|
"eslint": "^8.46.0",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"naive-ui": "^2.34.4",
|
||||||
|
"pinia": "^2.1.6",
|
||||||
|
"rollup-plugin-visualizer": "^5.9.2",
|
||||||
|
"sass": "^1.65.1",
|
||||||
|
"typescript": "^5.1.6",
|
||||||
|
"unocss": "^0.55.0",
|
||||||
|
"unplugin-auto-import": "^0.16.6",
|
||||||
|
"unplugin-icons": "^0.16.5",
|
||||||
|
"unplugin-vue-components": "^0.25.1",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
|
"vite-plugin-html": "^3.2.0",
|
||||||
|
"vite-plugin-svg-icons": "^2.0.1",
|
||||||
|
"vue": "^3.3.4",
|
||||||
|
"vue-i18n": "9",
|
||||||
|
"vue-router": "^4.2.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^4.2.3",
|
||||||
|
"vite": "^4.4.6"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,vue}": [
|
||||||
|
"eslint --ext .js,.vue ."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"@zclzone",
|
||||||
|
"@unocss",
|
||||||
|
".eslint-global-variables.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
5290
web1/pnpm-lock.yaml
generated
Normal file
1
web1/public/favicon.svg
Normal file
|
After Width: | Height: | Size: 10 KiB |
91
web1/public/resource/loading.css
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
.loading-container {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-svg {
|
||||||
|
width: 128px;
|
||||||
|
height: 128px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spin__container {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
margin: 36px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spin {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
animation: loadingSpin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-0 {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.right-0 {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.top-0 {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.bottom-0 {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spin-item {
|
||||||
|
position: absolute;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
-webkit-animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loadingSpin {
|
||||||
|
from {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loadingPulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-delay-500 {
|
||||||
|
-webkit-animation-delay: 500ms;
|
||||||
|
animation-delay: 500ms;
|
||||||
|
}
|
||||||
|
.loading-delay-1000 {
|
||||||
|
-webkit-animation-delay: 1000ms;
|
||||||
|
animation-delay: 1000ms;
|
||||||
|
}
|
||||||
|
.loading-delay-1500 {
|
||||||
|
-webkit-animation-delay: 1500ms;
|
||||||
|
animation-delay: 1500ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #6a6a6a;
|
||||||
|
}
|
||||||
25
web1/public/resource/loading.js
Normal file
1
web1/settings/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './theme.json'
|
||||||
37
web1/settings/theme.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"height": 60
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"visible": true,
|
||||||
|
"height": 50
|
||||||
|
},
|
||||||
|
"naiveThemeOverrides": {
|
||||||
|
"common": {
|
||||||
|
"primaryColor": "#F4511E",
|
||||||
|
"primaryColorHover": "#F4511E",
|
||||||
|
"primaryColorPressed": "#2B4C59FF",
|
||||||
|
"primaryColorSuppl": "#F4511E",
|
||||||
|
|
||||||
|
"infoColor": "#2080F0FF",
|
||||||
|
"infoColorHover": "#4098FCFF",
|
||||||
|
"infoColorPressed": "#1060C9FF",
|
||||||
|
"infoColorSuppl": "#4098FCFF",
|
||||||
|
|
||||||
|
"successColor": "#18A058FF",
|
||||||
|
"successColorHover": "#F4511E",
|
||||||
|
"successColorPressed": "#0C7A43FF",
|
||||||
|
"successColorSuppl": "#F4511E",
|
||||||
|
|
||||||
|
"warningColor": "#F0A020FF",
|
||||||
|
"warningColorHover": "#FCB040FF",
|
||||||
|
"warningColorPressed": "#C97C10FF",
|
||||||
|
"warningColorSuppl": "#FCB040FF",
|
||||||
|
|
||||||
|
"errorColor": "#D03050FF",
|
||||||
|
"errorColorHover": "#DE576DFF",
|
||||||
|
"errorColorPressed": "#AB1F3FFF",
|
||||||
|
"errorColorSuppl": "#DE576DFF"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
web1/src/App.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<AppProvider>
|
||||||
|
<router-view v-slot="{ Component }">
|
||||||
|
<component :is="Component" />
|
||||||
|
</router-view>
|
||||||
|
</AppProvider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import AppProvider from '@/components/common/AppProvider.vue'
|
||||||
|
</script>
|
||||||
74
web1/src/api/index.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { request } from '@/utils'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
login: (data) => request.post('/base/access_token', data, { noNeedToken: true }),
|
||||||
|
getUserInfo: () => request.get('/base/userinfo'),
|
||||||
|
getUserMenu: () => request.get('/base/usermenu'),
|
||||||
|
getUserApi: () => request.get('/base/userapi'),
|
||||||
|
// 手机号
|
||||||
|
registerPhone: (data) => request.post('/app-user/register', data, { noNeedToken: true }),
|
||||||
|
loginPhone: (data) => request.post('/app-user/login', data, { noNeedToken: true }),
|
||||||
|
// pages
|
||||||
|
getIndustryList: () => request.get('/industry/list'),
|
||||||
|
getHistoryList: (params) => request.get('/app-valuations/', { params }),
|
||||||
|
valuations: (data = {}) => request.post('/app-valuations/', data),
|
||||||
|
deleteValuations: (params = {}) => request.delete(`/app-valuations/${params.id}`),
|
||||||
|
// profile
|
||||||
|
updatePassword: (data = {}) => request.post('/base/update_password', data),
|
||||||
|
// users
|
||||||
|
getUserList: (params = {}) => request.get('/user/list', { params }),
|
||||||
|
getUserById: (params = {}) => request.get('/user/get', { params }),
|
||||||
|
createUser: (data = {}) => request.post('/user/create', data),
|
||||||
|
updateUser: (data = {}) => request.post('/user/update', data),
|
||||||
|
deleteUser: (params = {}) => request.delete(`/user/delete`, { params }),
|
||||||
|
resetPassword: (data = {}) => request.post(`/user/reset_password`, data),
|
||||||
|
// role
|
||||||
|
getRoleList: (params = {}) => request.get('/role/list', { params }),
|
||||||
|
createRole: (data = {}) => request.post('/role/create', data),
|
||||||
|
updateRole: (data = {}) => request.post('/role/update', data),
|
||||||
|
deleteRole: (params = {}) => request.delete('/role/delete', { params }),
|
||||||
|
updateRoleAuthorized: (data = {}) => request.post('/role/authorized', data),
|
||||||
|
getRoleAuthorized: (params = {}) => request.get('/role/authorized', { params }),
|
||||||
|
// menus
|
||||||
|
getMenus: (params = {}) => request.get('/menu/list', { params }),
|
||||||
|
createMenu: (data = {}) => request.post('/menu/create', data),
|
||||||
|
updateMenu: (data = {}) => request.post('/menu/update', data),
|
||||||
|
deleteMenu: (params = {}) => request.delete('/menu/delete', { params }),
|
||||||
|
// apis
|
||||||
|
getApis: (params = {}) => request.get('/api/list', { params }),
|
||||||
|
createApi: (data = {}) => request.post('/api/create', data),
|
||||||
|
updateApi: (data = {}) => request.post('/api/update', data),
|
||||||
|
deleteApi: (params = {}) => request.delete('/api/delete', { params }),
|
||||||
|
refreshApi: (data = {}) => request.post('/api/refresh', data),
|
||||||
|
// depts
|
||||||
|
getDepts: (params = {}) => request.get('/dept/list', { params }),
|
||||||
|
createDept: (data = {}) => request.post('/dept/create', data),
|
||||||
|
updateDept: (data = {}) => request.post('/dept/update', data),
|
||||||
|
deleteDept: (params = {}) => request.delete('/dept/delete', { params }),
|
||||||
|
// auditlog
|
||||||
|
getAuditLogList: (params = {}) => request.get('/auditlog/list', { params }),
|
||||||
|
// esg
|
||||||
|
getESGList: (params = {}) => request.get('/esg/list', { params }),
|
||||||
|
getESGById: (params = {}) => request.get('/esg/get', { params }),
|
||||||
|
createESG: (data = {}) => request.post('/esg/create', data),
|
||||||
|
updateESG: (data = {}) => request.post('/esg/update', data),
|
||||||
|
deleteESG: (params = {}) => request.delete('/esg/delete', { params }),
|
||||||
|
// index
|
||||||
|
getIndexList: (params = {}) => request.get('/index/list', { params }),
|
||||||
|
getIndexById: (params = {}) => request.get('/index/get', { params }),
|
||||||
|
createIndex: (data = {}) => request.post('/index/create', data),
|
||||||
|
updateIndex: (data = {}) => request.post('/index/update', data),
|
||||||
|
deleteIndex: (params = {}) => request.delete('/index/delete', { params }),
|
||||||
|
// industry
|
||||||
|
getIndustryList: (params = {}) => request.get('/industry/list', { params }),
|
||||||
|
getIndustryById: (params = {}) => request.get('/industry/get', { params }),
|
||||||
|
createIndustry: (data = {}) => request.post('/industry/create', data),
|
||||||
|
updateIndustry: (data = {}) => request.post('/industry/update', data),
|
||||||
|
deleteIndustry: (params = {}) => request.delete('/industry/delete', { params }),
|
||||||
|
// policy
|
||||||
|
getPolicyList: (params = {}) => request.get('/policy/list', { params }),
|
||||||
|
getPolicyById: (params = {}) => request.get('/policy/get', { params }),
|
||||||
|
createPolicy: (data = {}) => request.post('/policy/create', data),
|
||||||
|
updatePolicy: (data = {}) => request.post('/policy/update', data),
|
||||||
|
deletePolicy: (params = {}) => request.delete('/policy/delete', { params }),
|
||||||
|
}
|
||||||
BIN
web1/src/assets/images/background.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
web1/src/assets/images/bgc.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
web1/src/assets/images/bgg.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
web1/src/assets/images/bgy.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
web1/src/assets/images/circlec.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
web1/src/assets/images/circleg.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
web1/src/assets/images/circley.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
web1/src/assets/images/cost.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
web1/src/assets/images/cost1.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
web1/src/assets/images/cost2.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
web1/src/assets/images/delete.png
Normal file
|
After Width: | Height: | Size: 642 B |
BIN
web1/src/assets/images/fail.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
web1/src/assets/images/go.png
Normal file
|
After Width: | Height: | Size: 472 B |
BIN
web1/src/assets/images/loading.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
web1/src/assets/images/login_bg.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
web1/src/assets/images/login_bg.webp
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
web1/src/assets/images/logo.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
web1/src/assets/images/phone.png
Normal file
|
After Width: | Height: | Size: 662 B |
BIN
web1/src/assets/images/ps.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
web1/src/assets/images/retry.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
web1/src/assets/images/upload.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
230
web1/src/assets/js/icons.js
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
export default [
|
||||||
|
'mdi-air-humidifier-off',
|
||||||
|
'mdi-chili-off',
|
||||||
|
'mdi-cigar-off',
|
||||||
|
'mdi-clock-time-eight',
|
||||||
|
'mdi-clock-time-eight-outline',
|
||||||
|
'mdi-clock-time-eleven',
|
||||||
|
'mdi-clock-time-eleven-outline',
|
||||||
|
'mdi-clock-time-five',
|
||||||
|
'mdi-clock-time-five-outline',
|
||||||
|
'mdi-clock-time-four',
|
||||||
|
'mdi-clock-time-four-outline',
|
||||||
|
'mdi-clock-time-nine',
|
||||||
|
'mdi-clock-time-nine-outline',
|
||||||
|
'mdi-clock-time-one',
|
||||||
|
'mdi-clock-time-one-outline',
|
||||||
|
'mdi-clock-time-seven',
|
||||||
|
'mdi-clock-time-seven-outline',
|
||||||
|
'mdi-clock-time-six',
|
||||||
|
'mdi-clock-time-six-outline',
|
||||||
|
'mdi-clock-time-ten',
|
||||||
|
'mdi-clock-time-ten-outline',
|
||||||
|
'mdi-clock-time-three',
|
||||||
|
'mdi-clock-time-three-outline',
|
||||||
|
'mdi-clock-time-twelve',
|
||||||
|
'mdi-clock-time-twelve-outline',
|
||||||
|
'mdi-clock-time-two',
|
||||||
|
'mdi-clock-time-two-outline',
|
||||||
|
'mdi-cog-refresh',
|
||||||
|
'mdi-cog-refresh-outline',
|
||||||
|
'mdi-cog-sync',
|
||||||
|
'mdi-cog-sync-outline',
|
||||||
|
'mdi-content-save-cog',
|
||||||
|
'mdi-content-save-cog-outline',
|
||||||
|
'mdi-cosine-wave',
|
||||||
|
'mdi-cube-off',
|
||||||
|
'mdi-cube-off-outline',
|
||||||
|
'mdi-dome-light',
|
||||||
|
'mdi-download-box',
|
||||||
|
'mdi-download-box-outline',
|
||||||
|
'mdi-download-circle',
|
||||||
|
'mdi-download-circle-outline',
|
||||||
|
'mdi-fan-alert',
|
||||||
|
'mdi-fan-chevron-down',
|
||||||
|
'mdi-fan-chevron-up',
|
||||||
|
'mdi-fan-minus',
|
||||||
|
'mdi-fan-plus',
|
||||||
|
'mdi-fan-remove',
|
||||||
|
'mdi-fan-speed-1',
|
||||||
|
'mdi-fan-speed-2',
|
||||||
|
'mdi-fan-speed-3',
|
||||||
|
'mdi-food-drumstick',
|
||||||
|
'mdi-food-drumstick-off',
|
||||||
|
'mdi-food-drumstick-off-outline',
|
||||||
|
'mdi-food-drumstick-outline',
|
||||||
|
'mdi-food-steak',
|
||||||
|
'mdi-food-steak-off',
|
||||||
|
'mdi-fuse-alert',
|
||||||
|
'mdi-fuse-off',
|
||||||
|
'mdi-heart-minus',
|
||||||
|
'mdi-heart-minus-outline',
|
||||||
|
'mdi-heart-off-outline',
|
||||||
|
'mdi-heart-plus',
|
||||||
|
'mdi-heart-plus-outline',
|
||||||
|
'mdi-heart-remove',
|
||||||
|
'mdi-heart-remove-outline',
|
||||||
|
'mdi-hours-24',
|
||||||
|
'mdi-incognito-circle',
|
||||||
|
'mdi-incognito-circle-off',
|
||||||
|
'mdi-lingerie',
|
||||||
|
'mdi-microwave-off',
|
||||||
|
'mdi-minus-circle-off',
|
||||||
|
'mdi-minus-circle-off-outline',
|
||||||
|
'mdi-motion-sensor-off',
|
||||||
|
'mdi-pail-minus',
|
||||||
|
'mdi-pail-minus-outline',
|
||||||
|
'mdi-pail-off',
|
||||||
|
'mdi-pail-off-outline',
|
||||||
|
'mdi-pail-outline',
|
||||||
|
'mdi-pail-plus',
|
||||||
|
'mdi-pail-plus-outline',
|
||||||
|
'mdi-pail-remove',
|
||||||
|
'mdi-pail-remove-outline',
|
||||||
|
'mdi-pine-tree-fire',
|
||||||
|
'mdi-power-plug-off-outline',
|
||||||
|
'mdi-power-plug-outline',
|
||||||
|
'mdi-printer-eye',
|
||||||
|
'mdi-printer-search',
|
||||||
|
'mdi-puzzle-check',
|
||||||
|
'mdi-puzzle-check-outline',
|
||||||
|
'mdi-rug',
|
||||||
|
'mdi-sawtooth-wave',
|
||||||
|
'mdi-set-square',
|
||||||
|
'mdi-smoking-pipe-off',
|
||||||
|
'mdi-spoon-sugar',
|
||||||
|
'mdi-square-wave',
|
||||||
|
'mdi-table-split-cell',
|
||||||
|
'mdi-ticket-percent-outline',
|
||||||
|
'mdi-triangle-wave',
|
||||||
|
'mdi-waveform',
|
||||||
|
'mdi-wizard-hat',
|
||||||
|
'mdi-ab-testing',
|
||||||
|
'mdi-abjad-arabic',
|
||||||
|
'mdi-abjad-hebrew',
|
||||||
|
'mdi-abugida-devanagari',
|
||||||
|
'mdi-abugida-thai',
|
||||||
|
'mdi-access-point',
|
||||||
|
'mdi-access-point-network',
|
||||||
|
'mdi-access-point-network-off',
|
||||||
|
'mdi-account',
|
||||||
|
'mdi-account-alert',
|
||||||
|
'mdi-account-alert-outline',
|
||||||
|
'mdi-account-arrow-left',
|
||||||
|
'mdi-account-arrow-left-outline',
|
||||||
|
'mdi-account-arrow-right',
|
||||||
|
'mdi-account-arrow-right-outline',
|
||||||
|
'mdi-account-box',
|
||||||
|
'mdi-account-box-multiple',
|
||||||
|
'mdi-account-box-multiple-outline',
|
||||||
|
'mdi-account-box-outline',
|
||||||
|
'mdi-account-cancel',
|
||||||
|
'mdi-account-cancel-outline',
|
||||||
|
'mdi-account-cash',
|
||||||
|
'mdi-account-cash-outline',
|
||||||
|
'mdi-account-check',
|
||||||
|
'mdi-account-check-outline',
|
||||||
|
'mdi-account-child',
|
||||||
|
'mdi-account-child-circle',
|
||||||
|
'mdi-account-child-outline',
|
||||||
|
'mdi-account-circle',
|
||||||
|
'mdi-account-circle-outline',
|
||||||
|
'mdi-account-clock',
|
||||||
|
'mdi-account-clock-outline',
|
||||||
|
'mdi-account-cog',
|
||||||
|
'mdi-account-cog-outline',
|
||||||
|
'mdi-account-convert',
|
||||||
|
'mdi-account-convert-outline',
|
||||||
|
'mdi-account-cowboy-hat',
|
||||||
|
'mdi-account-details',
|
||||||
|
'mdi-account-details-outline',
|
||||||
|
'mdi-account-edit',
|
||||||
|
'mdi-account-edit-outline',
|
||||||
|
'mdi-account-group',
|
||||||
|
'mdi-account-group-outline',
|
||||||
|
'mdi-account-hard-hat',
|
||||||
|
'mdi-account-heart',
|
||||||
|
'mdi-account-heart-outline',
|
||||||
|
'mdi-account-key',
|
||||||
|
'mdi-account-key-outline',
|
||||||
|
'mdi-account-lock',
|
||||||
|
'mdi-account-lock-outline',
|
||||||
|
'mdi-account-minus',
|
||||||
|
'mdi-account-minus-outline',
|
||||||
|
'mdi-account-multiple',
|
||||||
|
'mdi-account-multiple-check',
|
||||||
|
'mdi-account-multiple-check-outline',
|
||||||
|
'mdi-account-multiple-minus',
|
||||||
|
'mdi-account-multiple-minus-outline',
|
||||||
|
'mdi-account-multiple-outline',
|
||||||
|
'mdi-account-multiple-plus',
|
||||||
|
'mdi-account-multiple-plus-outline',
|
||||||
|
'mdi-account-multiple-remove',
|
||||||
|
'mdi-account-multiple-remove-outline',
|
||||||
|
'mdi-account-music',
|
||||||
|
'mdi-account-music-outline',
|
||||||
|
'mdi-account-network',
|
||||||
|
'mdi-account-network-outline',
|
||||||
|
'mdi-account-off',
|
||||||
|
'mdi-account-off-outline',
|
||||||
|
'mdi-account-outline',
|
||||||
|
'mdi-account-plus',
|
||||||
|
'mdi-account-plus-outline',
|
||||||
|
'mdi-account-question',
|
||||||
|
'mdi-account-question-outline',
|
||||||
|
'mdi-account-remove',
|
||||||
|
'mdi-account-remove-outline',
|
||||||
|
'mdi-account-search',
|
||||||
|
'mdi-account-search-outline',
|
||||||
|
'mdi-account-settings',
|
||||||
|
'mdi-account-settings-outline',
|
||||||
|
'mdi-account-star',
|
||||||
|
'mdi-account-star-outline',
|
||||||
|
'mdi-account-supervisor',
|
||||||
|
'mdi-account-supervisor-circle',
|
||||||
|
'mdi-account-supervisor-outline',
|
||||||
|
'mdi-account-switch',
|
||||||
|
'mdi-account-switch-outline',
|
||||||
|
'mdi-account-tie',
|
||||||
|
'mdi-account-tie-outline',
|
||||||
|
'mdi-account-tie-voice',
|
||||||
|
'mdi-account-tie-voice-off',
|
||||||
|
'mdi-account-tie-voice-off-outline',
|
||||||
|
'mdi-account-tie-voice-outline',
|
||||||
|
'mdi-account-voice',
|
||||||
|
'mdi-adjust',
|
||||||
|
'mdi-adobe',
|
||||||
|
'mdi-adobe-acrobat',
|
||||||
|
'mdi-air-conditioner',
|
||||||
|
'mdi-air-filter',
|
||||||
|
'mdi-air-horn',
|
||||||
|
'mdi-air-humidifier',
|
||||||
|
'mdi-air-purifier',
|
||||||
|
'mdi-airbag',
|
||||||
|
'mdi-airballoon',
|
||||||
|
'mdi-airballoon-outline',
|
||||||
|
'mdi-airplane',
|
||||||
|
'mdi-airplane-landing',
|
||||||
|
'mdi-airplane-off',
|
||||||
|
'mdi-airplane-takeoff',
|
||||||
|
'mdi-airport',
|
||||||
|
'mdi-alarm',
|
||||||
|
'mdi-alarm-bell',
|
||||||
|
'mdi-alarm-check',
|
||||||
|
'mdi-alarm-light',
|
||||||
|
'mdi-alarm-light-outline',
|
||||||
|
'mdi-alarm-multiple',
|
||||||
|
'mdi-alarm-note',
|
||||||
|
'mdi-alarm-note-off',
|
||||||
|
'mdi-alarm-off',
|
||||||
|
'mdi-alarm-plus',
|
||||||
|
'mdi-alarm-snooze',
|
||||||
|
'mdi-album',
|
||||||
|
'mdi-alert',
|
||||||
|
'mdi-alert-box',
|
||||||
|
'mdi-alert-box-outline',
|
||||||
|
'mdi-alert-circle',
|
||||||
|
'mdi-alert-circle-check',
|
||||||
|
'mdi-alert-circle-check-outline',
|
||||||
|
'mdi-alert-circle-outline',
|
||||||
|
]
|
||||||
1
web1/src/assets/svg/forbidden.svg
Normal file
|
After Width: | Height: | Size: 60 KiB |
1
web1/src/assets/svg/front-page.svg
Normal file
|
After Width: | Height: | Size: 101 KiB |
1
web1/src/assets/svg/logo.svg
Normal file
|
After Width: | Height: | Size: 10 KiB |
1
web1/src/assets/svg/network-error.svg
Normal file
|
After Width: | Height: | Size: 86 KiB |
1
web1/src/assets/svg/no-data.svg
Normal file
|
After Width: | Height: | Size: 92 KiB |
1
web1/src/assets/svg/not-found.svg
Normal file
|
After Width: | Height: | Size: 71 KiB |
1
web1/src/assets/svg/server-error.svg
Normal file
|
After Width: | Height: | Size: 87 KiB |
1
web1/src/assets/svg/service-unavailable.svg
Normal file
|
After Width: | Height: | Size: 91 KiB |
1
web1/src/assets/svg/unauthorized.svg
Normal file
|
After Width: | Height: | Size: 60 KiB |
3
web1/src/components/common/AppFooter.vue
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<footer f-c-c flex-col text-14 color="#6a6a6a"></footer>
|
||||||
|
</template>
|
||||||
67
web1/src/components/common/AppProvider.vue
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<n-config-provider
|
||||||
|
wh-full
|
||||||
|
:locale="zhCN"
|
||||||
|
:date-locale="dateZhCN"
|
||||||
|
:theme="appStore.isDark ? darkTheme : undefined"
|
||||||
|
:theme-overrides="naiveThemeOverrides"
|
||||||
|
>
|
||||||
|
<n-loading-bar-provider>
|
||||||
|
<n-dialog-provider>
|
||||||
|
<n-notification-provider>
|
||||||
|
<n-message-provider>
|
||||||
|
<slot></slot>
|
||||||
|
<NaiveProviderContent />
|
||||||
|
</n-message-provider>
|
||||||
|
</n-notification-provider>
|
||||||
|
</n-dialog-provider>
|
||||||
|
</n-loading-bar-provider>
|
||||||
|
</n-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineComponent, h } from 'vue'
|
||||||
|
import {
|
||||||
|
zhCN,
|
||||||
|
dateZhCN,
|
||||||
|
darkTheme,
|
||||||
|
useLoadingBar,
|
||||||
|
useDialog,
|
||||||
|
useMessage,
|
||||||
|
useNotification,
|
||||||
|
} from 'naive-ui'
|
||||||
|
import { useCssVar } from '@vueuse/core'
|
||||||
|
import { kebabCase } from 'lodash-es'
|
||||||
|
import { setupMessage, setupDialog } from '@/utils'
|
||||||
|
import { naiveThemeOverrides } from '~/settings'
|
||||||
|
import { useAppStore } from '@/store'
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
function setupCssVar() {
|
||||||
|
const common = naiveThemeOverrides.common
|
||||||
|
for (const key in common) {
|
||||||
|
useCssVar(`--${kebabCase(key)}`, document.documentElement).value = common[key] || ''
|
||||||
|
if (key === 'primaryColor') window.localStorage.setItem('__THEME_COLOR__', common[key] || '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 挂载naive组件的方法至window, 以便在全局使用
|
||||||
|
function setupNaiveTools() {
|
||||||
|
window.$loadingBar = useLoadingBar()
|
||||||
|
window.$notification = useNotification()
|
||||||
|
|
||||||
|
window.$message = setupMessage(useMessage())
|
||||||
|
window.$dialog = setupDialog(useDialog())
|
||||||
|
}
|
||||||
|
|
||||||
|
const NaiveProviderContent = defineComponent({
|
||||||
|
setup() {
|
||||||
|
setupCssVar()
|
||||||
|
setupNaiveTools()
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return h('div')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
82
web1/src/components/common/LoadingEmptyWrapper.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="reloadFlag" class="relative">
|
||||||
|
<slot></slot>
|
||||||
|
<div v-show="showPlaceholder" class="absolute-lt h-full w-full" :class="placeholderClass">
|
||||||
|
<div v-show="loading" class="absolute-center">
|
||||||
|
<n-spin :show="true" :size="loadingSize" />
|
||||||
|
</div>
|
||||||
|
<div v-show="isEmpty" class="absolute-center">
|
||||||
|
<div class="relative">
|
||||||
|
<icon-custom-no-data :class="iconClass" />
|
||||||
|
<p class="absolute-lb w-full text-center" :class="descClass">{{ emptyDesc }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="!network" class="absolute-center">
|
||||||
|
<div
|
||||||
|
class="relative"
|
||||||
|
:class="{ 'cursor-pointer': showNetworkReload }"
|
||||||
|
@click="handleReload"
|
||||||
|
>
|
||||||
|
<icon-custom-network-error :class="iconClass" />
|
||||||
|
<p class="absolute-lb w-full text-center" :class="descClass">{{ networkErrorDesc }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, nextTick, watch, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'LoadingEmptyWrapper' })
|
||||||
|
|
||||||
|
const NETWORK_ERROR_MSG = '网络似乎开了小差~'
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
loading: false,
|
||||||
|
empty: false,
|
||||||
|
loadingSize: 'medium',
|
||||||
|
placeholderClass: 'bg-white dark:bg-dark transition-background-color duration-300 ease-in-out',
|
||||||
|
emptyDesc: '暂无数据',
|
||||||
|
iconClass: 'text-320px text-primary',
|
||||||
|
descClass: 'text-16px text-#666',
|
||||||
|
showNetworkReload: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 网络状态
|
||||||
|
const network = ref(window.navigator.onLine)
|
||||||
|
const reloadFlag = ref(true)
|
||||||
|
|
||||||
|
// 数据是否为空
|
||||||
|
const isEmpty = computed(() => props.empty && !props.loading && network.value)
|
||||||
|
|
||||||
|
const showPlaceholder = computed(() => props.loading || isEmpty.value || !network.value)
|
||||||
|
|
||||||
|
const networkErrorDesc = computed(() =>
|
||||||
|
props.showNetworkReload ? `${NETWORK_ERROR_MSG}, 点击重试` : NETWORK_ERROR_MSG,
|
||||||
|
)
|
||||||
|
|
||||||
|
function handleReload() {
|
||||||
|
if (!props.showNetworkReload) return
|
||||||
|
reloadFlag.value = false
|
||||||
|
nextTick(() => {
|
||||||
|
reloadFlag.value = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopHandle = watch(
|
||||||
|
() => props.loading,
|
||||||
|
(newValue) => {
|
||||||
|
// 结束加载判断一下网络状态
|
||||||
|
if (!newValue) {
|
||||||
|
network.value = window.navigator.onLine
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopHandle()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
160
web1/src/components/common/ScrollX.vue
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="wrapper" class="wrapper" @mousewheel.prevent="handleMouseWheel">
|
||||||
|
<template v-if="showArrow && isOverflow">
|
||||||
|
<div class="left" @click="handleMouseWheel({ wheelDelta: 120 })">
|
||||||
|
<icon-ic:baseline-keyboard-arrow-left />
|
||||||
|
</div>
|
||||||
|
<div class="right" @click="handleMouseWheel({ wheelDelta: -120 })">
|
||||||
|
<icon-ic:baseline-keyboard-arrow-right />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref="content"
|
||||||
|
v-resize="refreshIsOverflow"
|
||||||
|
class="content"
|
||||||
|
:class="{ overflow: isOverflow && showArrow }"
|
||||||
|
:style="{
|
||||||
|
transform: `translateX(${translateX}px)`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { debounce, useResize } from '@/utils'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
showArrow: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const translateX = ref(0)
|
||||||
|
const content = ref(null)
|
||||||
|
const wrapper = ref(null)
|
||||||
|
const isOverflow = ref(false)
|
||||||
|
|
||||||
|
const refreshIsOverflow = debounce(() => {
|
||||||
|
const wrapperWidth = wrapper.value?.offsetWidth
|
||||||
|
const contentWidth = content.value?.offsetWidth
|
||||||
|
isOverflow.value = contentWidth > wrapperWidth
|
||||||
|
resetTranslateX(wrapperWidth, contentWidth)
|
||||||
|
}, 200)
|
||||||
|
|
||||||
|
function handleMouseWheel(e) {
|
||||||
|
const { wheelDelta } = e
|
||||||
|
const wrapperWidth = wrapper.value?.offsetWidth
|
||||||
|
const contentWidth = content.value?.offsetWidth
|
||||||
|
/**
|
||||||
|
* @wheelDelta 平行滚动的值 >0: 右移 <0: 左移
|
||||||
|
* @translateX 内容translateX的值
|
||||||
|
* @wrapperWidth 容器的宽度
|
||||||
|
* @contentWidth 内容的宽度
|
||||||
|
*/
|
||||||
|
if (wheelDelta < 0) {
|
||||||
|
if (wrapperWidth > contentWidth && translateX.value < -10) return
|
||||||
|
if (wrapperWidth <= contentWidth && contentWidth + translateX.value - wrapperWidth < -10) return
|
||||||
|
}
|
||||||
|
if (wheelDelta > 0 && translateX.value > 10) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
translateX.value += wheelDelta
|
||||||
|
resetTranslateX(wrapperWidth, contentWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetTranslateX = debounce(function (wrapperWidth, contentWidth) {
|
||||||
|
if (!isOverflow.value) {
|
||||||
|
translateX.value = 0
|
||||||
|
} else if (-translateX.value > contentWidth - wrapperWidth) {
|
||||||
|
translateX.value = wrapperWidth - contentWidth
|
||||||
|
} else if (translateX.value > 0) {
|
||||||
|
translateX.value = 0
|
||||||
|
}
|
||||||
|
}, 200)
|
||||||
|
|
||||||
|
const observer = ref(null)
|
||||||
|
onMounted(() => {
|
||||||
|
refreshIsOverflow()
|
||||||
|
|
||||||
|
observer.value = useResize(document.body, refreshIsOverflow)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
observer.value?.disconnect()
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleScroll(x, width) {
|
||||||
|
const wrapperWidth = wrapper.value?.offsetWidth
|
||||||
|
const contentWidth = content.value?.offsetWidth
|
||||||
|
if (contentWidth <= wrapperWidth) return
|
||||||
|
|
||||||
|
// 当 x 小于可视范围的最小值时
|
||||||
|
if (x < -translateX.value + 150) {
|
||||||
|
translateX.value = -(x - 150)
|
||||||
|
resetTranslateX(wrapperWidth, contentWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当 x 大于可视范围的最大值时
|
||||||
|
if (x + width > -translateX.value + wrapperWidth) {
|
||||||
|
translateX.value = wrapperWidth - (x + width)
|
||||||
|
resetTranslateX(wrapperWidth, contentWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
handleScroll,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
z-index: 9;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
.content {
|
||||||
|
padding: 0 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
transition: transform 0.5s;
|
||||||
|
&.overflow {
|
||||||
|
padding-left: 30px;
|
||||||
|
padding-right: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.left,
|
||||||
|
.right {
|
||||||
|
background-color: #fff;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
width: 20px;
|
||||||
|
height: 35px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
font-size: 18px;
|
||||||
|
border: 1px solid #e0e0e6;
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
z-index: 2;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
22
web1/src/components/icon/CustomIcon.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<script setup>
|
||||||
|
/** 自定义图标 */
|
||||||
|
const props = defineProps({
|
||||||
|
/** 图标名称(assets/svg下的文件名) */
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 14,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TheIcon type="custom" v-bind="props" />
|
||||||
|
</template>
|
||||||
70
web1/src/components/icon/IconPicker.vue
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { watchDebounced } from '@vueuse/core'
|
||||||
|
import { NInput, NPopover } from 'naive-ui'
|
||||||
|
|
||||||
|
import TheIcon from './TheIcon.vue'
|
||||||
|
import iconData from '@/assets/js/icons'
|
||||||
|
|
||||||
|
const props = defineProps({ value: String })
|
||||||
|
const emit = defineEmits(['update:value'])
|
||||||
|
|
||||||
|
const choosed = ref(props.value) // 选中值
|
||||||
|
const icons = ref(iconData)
|
||||||
|
// const icons = ref(iconData.filter((icon) => icon.includes(choosed.value))) // 可选图标列表
|
||||||
|
|
||||||
|
function filterIcons() {
|
||||||
|
icons.value = iconData.filter((item) => item.includes(choosed.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectIcon(icon) {
|
||||||
|
choosed.value = icon
|
||||||
|
emit('update:value', choosed.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
watchDebounced(
|
||||||
|
choosed,
|
||||||
|
() => {
|
||||||
|
filterIcons()
|
||||||
|
emit('update:value', choosed.value)
|
||||||
|
},
|
||||||
|
{ debounce: 200 },
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full">
|
||||||
|
<NPopover trigger="click" placement="bottom-start">
|
||||||
|
<template #trigger>
|
||||||
|
<NInput v-model:value="choosed" placeholder="请输入图标名称" @update:value="filterIcons">
|
||||||
|
<template #prefix>
|
||||||
|
<span class="i-mdi:magnify text-18" />
|
||||||
|
</template>
|
||||||
|
<template #suffix>
|
||||||
|
<TheIcon :icon="choosed" :size="18" />
|
||||||
|
</template>
|
||||||
|
</NInput>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
更多图标去
|
||||||
|
<a class="text-blue" target="_blank" href="https://icones.js.org/collection/all">
|
||||||
|
Icones
|
||||||
|
</a>
|
||||||
|
查看
|
||||||
|
</template>
|
||||||
|
<ul v-if="icons.length" class="h-150 w-300 overflow-y-scroll">
|
||||||
|
<li
|
||||||
|
v-for="(icon, index) in icons"
|
||||||
|
:key="index"
|
||||||
|
class="mx-5 inline-block cursor-pointer hover:text-cyan"
|
||||||
|
@click="selectIcon(icon)"
|
||||||
|
>
|
||||||
|
<TheIcon :icon="icon" :size="18" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div v-else>
|
||||||
|
<TheIcon :icon="choosed" :size="18" />
|
||||||
|
</div>
|
||||||
|
</NPopover>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
24
web1/src/components/icon/SvgIcon.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
prefix: {
|
||||||
|
type: String,
|
||||||
|
default: 'icon-custom',
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: 'currentColor',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const symbolId = computed(() => `#${props.prefix}-${props.icon}`)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg aria-hidden="true" width="1em" height="1em">
|
||||||
|
<use :xlink:href="symbolId" :fill="color" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
22
web1/src/components/icon/TheIcon.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<script setup>
|
||||||
|
import { renderIcon } from '@/utils'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 14,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component :is="renderIcon(icon, { size, color })" />
|
||||||
|
</template>
|
||||||
18
web1/src/components/page/AppPage.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="fade-slide" mode="out-in" appear>
|
||||||
|
<section class="cus-scroll-y wh-full flex-col bg-[#f5f6fb] p-15 dark:bg-hex-121212">
|
||||||
|
<slot />
|
||||||
|
<AppFooter v-if="showFooter" mt-15 />
|
||||||
|
<n-back-top :bottom="20" />
|
||||||
|
</section>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
showFooter: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
33
web1/src/components/page/CommonPage.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<AppPage :show-footer="showFooter">
|
||||||
|
<header v-if="showHeader" mb-15 min-h-45 flex items-center justify-between px-15>
|
||||||
|
<slot v-if="$slots.header" name="header" />
|
||||||
|
<template v-else>
|
||||||
|
<h2 text-22 font-normal text-hex-333 dark:text-hex-ccc>{{ title || route.meta?.title }}</h2>
|
||||||
|
<slot name="action" />
|
||||||
|
</template>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<n-card flex-1 rounded-10>
|
||||||
|
<slot />
|
||||||
|
</n-card>
|
||||||
|
</AppPage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
showFooter: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showHeader: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const route = useRoute()
|
||||||
|
</script>
|
||||||
26
web1/src/components/query-bar/QueryBar.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
bg="#fafafc"
|
||||||
|
min-h-60
|
||||||
|
flex
|
||||||
|
items-start
|
||||||
|
justify-between
|
||||||
|
b-1
|
||||||
|
rounded-8
|
||||||
|
p-15
|
||||||
|
bc-ccc
|
||||||
|
dark:bg-black
|
||||||
|
>
|
||||||
|
<n-space wrap :size="[35, 15]">
|
||||||
|
<slot />
|
||||||
|
<div>
|
||||||
|
<n-button secondary type="primary" @click="emit('reset')">重置</n-button>
|
||||||
|
<n-button ml-20 type="primary" @click="emit('search')">搜索</n-button>
|
||||||
|
</div>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const emit = defineEmits(['search', 'reset'])
|
||||||
|
</script>
|
||||||
34
web1/src/components/query-bar/QueryBarItem.vue
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<div flex items-center>
|
||||||
|
<label
|
||||||
|
v-if="!isNullOrWhitespace(label)"
|
||||||
|
w-80
|
||||||
|
flex-shrink-0
|
||||||
|
:style="{ width: labelWidth + 'px' }"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { isNullOrWhitespace } from '@/utils'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
labelWidth: {
|
||||||
|
type: Number,
|
||||||
|
default: 80,
|
||||||
|
},
|
||||||
|
contentWidth: {
|
||||||
|
type: Number,
|
||||||
|
default: 220,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
56
web1/src/components/table/CrudModal.vue
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="show"
|
||||||
|
:style="{ width }"
|
||||||
|
preset="card"
|
||||||
|
:title="title"
|
||||||
|
size="huge"
|
||||||
|
:bordered="false"
|
||||||
|
:mask-closable="false"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<template v-if="showFooter" #footer>
|
||||||
|
<footer flex justify-end>
|
||||||
|
<slot name="footer">
|
||||||
|
<n-button @click="show = false">取消</n-button>
|
||||||
|
<n-button :loading="loading" ml-20 type="primary" @click="emit('save')">保存</n-button>
|
||||||
|
</slot>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
default: '600px',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
showFooter: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'onSave'])
|
||||||
|
const show = computed({
|
||||||
|
get() {
|
||||||
|
return props.visible
|
||||||
|
},
|
||||||
|
set(v) {
|
||||||
|
emit('update:visible', v)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
151
web1/src/components/table/CrudTable.vue
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<template>
|
||||||
|
<div v-bind="$attrs">
|
||||||
|
<QueryBar v-if="$slots.queryBar" mb-30 @search="handleSearch" @reset="handleReset">
|
||||||
|
<slot name="queryBar" />
|
||||||
|
</QueryBar>
|
||||||
|
|
||||||
|
<n-data-table
|
||||||
|
:remote="remote"
|
||||||
|
:loading="loading"
|
||||||
|
:columns="columns"
|
||||||
|
:data="tableData"
|
||||||
|
:scroll-x="scrollX"
|
||||||
|
:row-key="(row) => row[rowKey]"
|
||||||
|
:pagination="isPagination ? pagination : false"
|
||||||
|
@update:checked-row-keys="onChecked"
|
||||||
|
@update:page="onPageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
/**
|
||||||
|
* @remote true: 后端分页 false: 前端分页
|
||||||
|
*/
|
||||||
|
remote: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @remote 是否分页
|
||||||
|
*/
|
||||||
|
isPagination: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
scrollX: {
|
||||||
|
type: Number,
|
||||||
|
default: 450,
|
||||||
|
},
|
||||||
|
rowKey: {
|
||||||
|
type: String,
|
||||||
|
default: 'id',
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
/** queryBar中的参数 */
|
||||||
|
queryItems: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/** 补充参数(可选) */
|
||||||
|
extraParams: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* ! 约定接口入参出参
|
||||||
|
* * 分页模式需约定分页接口入参
|
||||||
|
* @page_size 分页参数:一页展示多少条,默认10
|
||||||
|
* @page 分页参数:页码,默认1
|
||||||
|
*/
|
||||||
|
getData: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:queryItems', 'onChecked', 'onDataChange'])
|
||||||
|
const loading = ref(false)
|
||||||
|
const initQuery = { ...props.queryItems }
|
||||||
|
const tableData = ref([])
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
page_size: 10,
|
||||||
|
pageSizes: [10, 20, 50, 100],
|
||||||
|
showSizePicker: true,
|
||||||
|
prefix({ itemCount }) {
|
||||||
|
return `共 ${itemCount} 条`
|
||||||
|
},
|
||||||
|
onChange: (page) => {
|
||||||
|
pagination.page = page
|
||||||
|
},
|
||||||
|
onUpdatePageSize: (pageSize) => {
|
||||||
|
pagination.page_size = pageSize
|
||||||
|
pagination.page = 1
|
||||||
|
handleQuery()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleQuery() {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
let paginationParams = {}
|
||||||
|
// 如果非分页模式或者使用前端分页,则无需传分页参数
|
||||||
|
if (props.isPagination && props.remote) {
|
||||||
|
paginationParams = { page: pagination.page, page_size: pagination.page_size }
|
||||||
|
}
|
||||||
|
const { data, total } = await props.getData({
|
||||||
|
...props.queryItems,
|
||||||
|
...props.extraParams,
|
||||||
|
...paginationParams,
|
||||||
|
})
|
||||||
|
tableData.value = data
|
||||||
|
pagination.itemCount = total || 0
|
||||||
|
} catch (error) {
|
||||||
|
tableData.value = []
|
||||||
|
pagination.itemCount = 0
|
||||||
|
} finally {
|
||||||
|
emit('onDataChange', tableData.value)
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleSearch() {
|
||||||
|
pagination.page = 1
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
async function handleReset() {
|
||||||
|
const queryItems = { ...props.queryItems }
|
||||||
|
for (const key in queryItems) {
|
||||||
|
queryItems[key] = null
|
||||||
|
}
|
||||||
|
emit('update:queryItems', { ...queryItems, ...initQuery })
|
||||||
|
await nextTick()
|
||||||
|
pagination.page = 1
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
function onPageChange(currentPage) {
|
||||||
|
pagination.page = currentPage
|
||||||
|
if (props.remote) {
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onChecked(rowKeys) {
|
||||||
|
if (props.columns.some((item) => item.type === 'selection')) {
|
||||||
|
emit('onChecked', rowKeys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
handleSearch,
|
||||||
|
handleReset,
|
||||||
|
tableData,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
1
web1/src/composables/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as useCRUD } from './useCRUD'
|
||||||
104
web1/src/composables/useCRUD.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { isNullOrWhitespace } from '@/utils'
|
||||||
|
|
||||||
|
const ACTIONS = {
|
||||||
|
view: '查看',
|
||||||
|
edit: '编辑',
|
||||||
|
add: '新增',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ({ name, initForm = {}, doCreate, doDelete, doUpdate, refresh }) {
|
||||||
|
const modalVisible = ref(false)
|
||||||
|
const modalAction = ref('')
|
||||||
|
const modalTitle = computed(() => ACTIONS[modalAction.value] + name)
|
||||||
|
const modalLoading = ref(false)
|
||||||
|
const modalFormRef = ref(null)
|
||||||
|
const modalForm = ref({ ...initForm })
|
||||||
|
|
||||||
|
/** 新增 */
|
||||||
|
function handleAdd() {
|
||||||
|
modalAction.value = 'add'
|
||||||
|
modalVisible.value = true
|
||||||
|
modalForm.value = { ...initForm }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改 */
|
||||||
|
function handleEdit(row) {
|
||||||
|
modalAction.value = 'edit'
|
||||||
|
modalVisible.value = true
|
||||||
|
modalForm.value = { ...row }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查看 */
|
||||||
|
function handleView(row) {
|
||||||
|
modalAction.value = 'view'
|
||||||
|
modalVisible.value = true
|
||||||
|
modalForm.value = { ...row }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存 */
|
||||||
|
function handleSave(...callbacks) {
|
||||||
|
if (!['edit', 'add'].includes(modalAction.value)) {
|
||||||
|
modalVisible.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
modalFormRef.value?.validate(async (err) => {
|
||||||
|
if (err) return
|
||||||
|
const actions = {
|
||||||
|
add: {
|
||||||
|
api: () => doCreate(modalForm.value),
|
||||||
|
cb: () => {
|
||||||
|
callbacks.forEach((callback) => callback && callback())
|
||||||
|
},
|
||||||
|
msg: () => $message.success('新增成功'),
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
api: () => doUpdate(modalForm.value),
|
||||||
|
cb: () => {
|
||||||
|
callbacks.forEach((callback) => callback && callback())
|
||||||
|
},
|
||||||
|
msg: () => $message.success('编辑成功'),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const action = actions[modalAction.value]
|
||||||
|
|
||||||
|
try {
|
||||||
|
modalLoading.value = true
|
||||||
|
const data = await action.api()
|
||||||
|
action.cb()
|
||||||
|
action.msg()
|
||||||
|
modalLoading.value = modalVisible.value = false
|
||||||
|
data && refresh(data)
|
||||||
|
} catch (error) {
|
||||||
|
modalLoading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除 */
|
||||||
|
async function handleDelete(params = {}) {
|
||||||
|
if (isNullOrWhitespace(params)) return
|
||||||
|
try {
|
||||||
|
modalLoading.value = true
|
||||||
|
const data = await doDelete(params)
|
||||||
|
$message.success('删除成功')
|
||||||
|
modalLoading.value = false
|
||||||
|
refresh(data)
|
||||||
|
} catch (error) {
|
||||||
|
modalLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
modalVisible,
|
||||||
|
modalAction,
|
||||||
|
modalTitle,
|
||||||
|
modalLoading,
|
||||||
|
handleAdd,
|
||||||
|
handleDelete,
|
||||||
|
handleEdit,
|
||||||
|
handleView,
|
||||||
|
handleSave,
|
||||||
|
modalForm,
|
||||||
|
modalFormRef,
|
||||||
|
}
|
||||||
|
}
|
||||||
6
web1/src/directives/index.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import setupPermissionDirective from './permission'
|
||||||
|
|
||||||
|
/** setup custom vue directives. - [安装自定义的vue指令] */
|
||||||
|
export function setupDirectives(app) {
|
||||||
|
setupPermissionDirective(app)
|
||||||
|
}
|
||||||
34
web1/src/directives/permission.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { useUserStore, usePermissionStore } from '@/store'
|
||||||
|
|
||||||
|
function hasPermission(permission) {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const userPermissionStore = usePermissionStore()
|
||||||
|
|
||||||
|
const accessApis = userPermissionStore.apis
|
||||||
|
if (userStore.isSuperUser) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return accessApis.includes(permission)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function setupPermissionDirective(app) {
|
||||||
|
function updateElVisible(el, permission) {
|
||||||
|
if (!permission) {
|
||||||
|
throw new Error(`need roles: like v-permission="get/api/v1/user/list"`)
|
||||||
|
}
|
||||||
|
if (!hasPermission(permission)) {
|
||||||
|
el.parentElement?.removeChild(el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissionDirective = {
|
||||||
|
mounted(el, binding) {
|
||||||
|
updateElVisible(el, binding.value)
|
||||||
|
},
|
||||||
|
beforeUpdate(el, binding) {
|
||||||
|
updateElVisible(el, binding.value)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
app.directive('permission', permissionDirective)
|
||||||
|
}
|
||||||
23
web1/src/layout/components/AppMain.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<router-view v-slot="{ Component, route }">
|
||||||
|
<KeepAlive :include="keepAliveRouteNames">
|
||||||
|
<component
|
||||||
|
:is="Component"
|
||||||
|
v-if="appStore.reloadFlag"
|
||||||
|
:key="appStore.aliveKeys[route.name] || route.fullPath"
|
||||||
|
/>
|
||||||
|
</KeepAlive>
|
||||||
|
</router-view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useAppStore } from '@/store'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const allRoutes = router.getRoutes()
|
||||||
|
const keepAliveRouteNames = computed(() => {
|
||||||
|
return allRoutes.filter((route) => route.meta?.keepAlive).map((route) => route.name)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
30
web1/src/layout/components/header/components/BreadCrumb.vue
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<n-breadcrumb>
|
||||||
|
<n-breadcrumb-item
|
||||||
|
v-for="item in route.matched.filter((item) => !!item.meta?.title)"
|
||||||
|
:key="item.path"
|
||||||
|
@click="handleBreadClick(item.path)"
|
||||||
|
>
|
||||||
|
<component :is="getIcon(item.meta)" />
|
||||||
|
{{ item.meta.title }}
|
||||||
|
</n-breadcrumb-item>
|
||||||
|
</n-breadcrumb>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { renderCustomIcon, renderIcon } from '@/utils'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
function handleBreadClick(path) {
|
||||||
|
if (path === route.path) return
|
||||||
|
router.push(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIcon(meta) {
|
||||||
|
if (meta?.customIcon) return renderCustomIcon(meta.customIcon, { size: 18 })
|
||||||
|
if (meta?.icon) return renderIcon(meta.icon, { size: 18 })
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
</script>
|
||||||
14
web1/src/layout/components/header/components/FullScreen.vue
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<n-icon v-if="appStore.fullScreen" mr20 size="18" style="cursor: pointer" @click="toggle">
|
||||||
|
<icon-ant-design:fullscreen-exit-outlined v-if="isFullscreen" />
|
||||||
|
<icon-ant-design:fullscreen-outlined v-else />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useFullscreen } from '@vueuse/core'
|
||||||
|
import { useAppStore } from '@/store'
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const { isFullscreen, toggle } = useFullscreen()
|
||||||
|
</script>
|
||||||
11
web1/src/layout/components/header/components/GithubSite.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<n-icon mr-20 size="18" style="cursor: pointer" @click="handleLinkClick">
|
||||||
|
<icon-mdi:github />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
function handleLinkClick() {
|
||||||
|
window.open('https://github.com/mizhexiaoxiao/vue-fastapi-admin')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
33
web1/src/layout/components/header/components/Languages.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<n-dropdown :options="options" @select="handleChangeLocale">
|
||||||
|
<n-icon mr-20 size="18" style="cursor: pointer">
|
||||||
|
<icon-mdi:globe />
|
||||||
|
</n-icon>
|
||||||
|
</n-dropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useAppStore } from '@/store'
|
||||||
|
import { router } from '~/src/router'
|
||||||
|
|
||||||
|
const store = useAppStore()
|
||||||
|
const { availableLocales, t } = useI18n()
|
||||||
|
|
||||||
|
const options = computed(() => {
|
||||||
|
let select = []
|
||||||
|
availableLocales.forEach((locale) => {
|
||||||
|
select.push({
|
||||||
|
label: t('lang', 1, { locale: locale }),
|
||||||
|
key: locale,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return select
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleChangeLocale = (value) => {
|
||||||
|
store.setLocale(value)
|
||||||
|
// reload page
|
||||||
|
router.go()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<n-icon size="20" cursor-pointer @click="appStore.switchCollapsed">
|
||||||
|
<icon-mdi:format-indent-increase v-if="appStore.collapsed" />
|
||||||
|
<icon-mdi:format-indent-decrease v-else />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useAppStore } from '@/store'
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
</script>
|
||||||
18
web1/src/layout/components/header/components/ThemeMode.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useAppStore } from '@/store'
|
||||||
|
import { useDark, useToggle } from '@vueuse/core'
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const isDark = useDark()
|
||||||
|
const toggleDark = () => {
|
||||||
|
appStore.toggleDark()
|
||||||
|
useToggle(isDark)()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-icon mr-20 cursor-pointer size="18" @click="toggleDark">
|
||||||
|
<icon-mdi-moon-waning-crescent v-if="isDark" />
|
||||||
|
<icon-mdi-white-balance-sunny v-else />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
50
web1/src/layout/components/header/components/UserAvatar.vue
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<n-dropdown :options="options" @select="handleSelect">
|
||||||
|
<div flex cursor-pointer items-center>
|
||||||
|
<img :src="userStore.avatar" mr10 h-35 w-35 rounded-full />
|
||||||
|
<span>{{ userStore.name }}</span>
|
||||||
|
</div>
|
||||||
|
</n-dropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useUserStore } from '@/store'
|
||||||
|
import { renderIcon } from '@/utils'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
label: t('header.label_profile'),
|
||||||
|
key: 'profile',
|
||||||
|
icon: renderIcon('mdi-account-arrow-right-outline', { size: '14px' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('header.label_logout'),
|
||||||
|
key: 'logout',
|
||||||
|
icon: renderIcon('mdi:exit-to-app', { size: '14px' }),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function handleSelect(key) {
|
||||||
|
if (key === 'profile') {
|
||||||
|
router.push('/profile')
|
||||||
|
} else if (key === 'logout') {
|
||||||
|
$dialog.confirm({
|
||||||
|
title: t('header.label_logout_dialog_title'),
|
||||||
|
type: 'warning',
|
||||||
|
content: t('header.text_logout_confirm'),
|
||||||
|
confirm() {
|
||||||
|
userStore.logout()
|
||||||
|
$message.success(t('header.text_logout_success'))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
23
web1/src/layout/components/header/index.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<div flex items-center>
|
||||||
|
<MenuCollapse />
|
||||||
|
<BreadCrumb ml-15 hidden sm:block />
|
||||||
|
</div>
|
||||||
|
<div ml-auto flex items-center>
|
||||||
|
<Languages />
|
||||||
|
<ThemeMode />
|
||||||
|
<GithubSite />
|
||||||
|
<FullScreen />
|
||||||
|
<UserAvatar />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import BreadCrumb from './components/BreadCrumb.vue'
|
||||||
|
import MenuCollapse from './components/MenuCollapse.vue'
|
||||||
|
import FullScreen from './components/FullScreen.vue'
|
||||||
|
import UserAvatar from './components/UserAvatar.vue'
|
||||||
|
import GithubSite from './components/GithubSite.vue'
|
||||||
|
import ThemeMode from './components/ThemeMode.vue'
|
||||||
|
import Languages from './components/Languages.vue'
|
||||||
|
</script>
|
||||||
24
web1/src/layout/components/sidebar/components/SideLogo.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<router-link h-60 f-c-c to="/">
|
||||||
|
<icon-custom-logo text-36 color-primary></icon-custom-logo>
|
||||||
|
<h2
|
||||||
|
v-show="!appStore.collapsed"
|
||||||
|
ml-2
|
||||||
|
mr-8
|
||||||
|
max-w-150
|
||||||
|
flex-shrink-0
|
||||||
|
text-16
|
||||||
|
font-bold
|
||||||
|
color-primary
|
||||||
|
>
|
||||||
|
{{ title }}
|
||||||
|
</h2>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useAppStore } from '@/store'
|
||||||
|
const title = import.meta.env.VITE_TITLE
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
</script>
|
||||||
125
web1/src/layout/components/sidebar/components/SideMenu.vue
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<template>
|
||||||
|
<n-menu
|
||||||
|
ref="menu"
|
||||||
|
class="side-menu"
|
||||||
|
accordion
|
||||||
|
:indent="18"
|
||||||
|
:collapsed-icon-size="22"
|
||||||
|
:collapsed-width="64"
|
||||||
|
:options="menuOptions"
|
||||||
|
:value="activeKey"
|
||||||
|
@update:value="handleMenuSelect"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { usePermissionStore, useAppStore } from '@/store'
|
||||||
|
import { renderCustomIcon, renderIcon, isExternal } from '@/utils'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const curRoute = useRoute()
|
||||||
|
const permissionStore = usePermissionStore()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
const activeKey = computed(() => curRoute.meta?.activeMenu || curRoute.name)
|
||||||
|
|
||||||
|
const menuOptions = computed(() => {
|
||||||
|
return permissionStore.menus.map((item) => getMenuItem(item)).sort((a, b) => a.order - b.order)
|
||||||
|
})
|
||||||
|
|
||||||
|
const menu = ref(null)
|
||||||
|
watch(curRoute, async () => {
|
||||||
|
await nextTick()
|
||||||
|
menu.value?.showOption()
|
||||||
|
})
|
||||||
|
|
||||||
|
function resolvePath(basePath, path) {
|
||||||
|
if (isExternal(path)) return path
|
||||||
|
return (
|
||||||
|
'/' +
|
||||||
|
[basePath, path]
|
||||||
|
.filter((path) => !!path && path !== '/')
|
||||||
|
.map((path) => path.replace(/(^\/)|(\/$)/g, ''))
|
||||||
|
.join('/')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMenuItem(route, basePath = '') {
|
||||||
|
let menuItem = {
|
||||||
|
label: (route.meta && route.meta.title) || route.name,
|
||||||
|
key: route.name,
|
||||||
|
path: resolvePath(basePath, route.path),
|
||||||
|
icon: getIcon(route.meta),
|
||||||
|
order: route.meta?.order || 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleChildren = route.children
|
||||||
|
? route.children.filter((item) => item.name && !item.isHidden)
|
||||||
|
: []
|
||||||
|
|
||||||
|
if (!visibleChildren.length) return menuItem
|
||||||
|
|
||||||
|
if (visibleChildren.length === 1) {
|
||||||
|
// 单个子路由处理
|
||||||
|
const singleRoute = visibleChildren[0]
|
||||||
|
menuItem = {
|
||||||
|
...menuItem,
|
||||||
|
label: singleRoute.meta?.title || singleRoute.name,
|
||||||
|
key: singleRoute.name,
|
||||||
|
path: resolvePath(menuItem.path, singleRoute.path),
|
||||||
|
icon: getIcon(singleRoute.meta),
|
||||||
|
}
|
||||||
|
const visibleItems = singleRoute.children
|
||||||
|
? singleRoute.children.filter((item) => item.name && !item.isHidden)
|
||||||
|
: []
|
||||||
|
|
||||||
|
if (visibleItems.length === 1) {
|
||||||
|
menuItem = getMenuItem(visibleItems[0], menuItem.path)
|
||||||
|
} else if (visibleItems.length > 1) {
|
||||||
|
menuItem.children = visibleItems
|
||||||
|
.map((item) => getMenuItem(item, menuItem.path))
|
||||||
|
.sort((a, b) => a.order - b.order)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
menuItem.children = visibleChildren
|
||||||
|
.map((item) => getMenuItem(item, menuItem.path))
|
||||||
|
.sort((a, b) => a.order - b.order)
|
||||||
|
}
|
||||||
|
return menuItem
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIcon(meta) {
|
||||||
|
if (meta?.customIcon) return renderCustomIcon(meta.customIcon, { size: 18 })
|
||||||
|
if (meta?.icon) return renderIcon(meta.icon, { size: 18 })
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMenuSelect(key, item) {
|
||||||
|
if (isExternal(item.path)) {
|
||||||
|
window.open(item.path)
|
||||||
|
} else {
|
||||||
|
if (item.path === curRoute.path) {
|
||||||
|
appStore.reloadPage()
|
||||||
|
} else {
|
||||||
|
router.push(item.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.side-menu:not(.n-menu--collapsed) {
|
||||||
|
.n-menu-item-content {
|
||||||
|
&::before {
|
||||||
|
left: 5px;
|
||||||
|
right: 5px;
|
||||||
|
}
|
||||||
|
&.n-menu-item-content--selected,
|
||||||
|
&:hover {
|
||||||
|
&::before {
|
||||||
|
border-left: 4px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||