init: 初始化

This commit is contained in:
华珑 2025-02-12 18:37:04 +08:00
commit 332a3fbeac
118 changed files with 6407 additions and 0 deletions

22
.env.development Normal file
View File

@ -0,0 +1,22 @@
NODE_ENV=development
# 指定站点名称
# 本地开发状态localhost
# 部署时:大多数情况下不需要填写
VITE_SITEHOST=localhost
# 启动站点API接口地址
VITE_SITEHOST_API=http://47.103.207.8:8132
# 路由根地址
VITE_ROUTER_BASE=/
# 应用码
VITE_APPCODE=hospital
VITE_TOOL_ICONS=//at.alicdn.com/t/c/font_4255235_cqom7h3serb.js
VITE_MICROLAYOUT_ICONS=//at.alicdn.com/t/c/font_4255235_cqom7h3serb.js
# 是否开启登录测试
VITE_LOGIN_TEST=false
# 登录测试账号
VITE_LOGIN_TEST_USERNAME=admin
# 登录测试账号密码
VITE_LOGIN_TEST_PASSWORD="234@#$"

5
.env.production Normal file
View File

@ -0,0 +1,5 @@
# 路由根地址
VITE_ROUTER_BASE=/
# 应用码
VITE_APPCODE=hospital

23
.eslintrc.js Normal file
View File

@ -0,0 +1,23 @@
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:vue/vue3-essential',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parser: '@typescript-eslint/parser',
},
plugins: ['vue', '@typescript-eslint', 'prettier'],
rules: {
'prettier/prettier': 'error',
},
};

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
node_modules
dist
source
pnpm-lock.yaml
visualizer.html
tsconfig.tsbuildinfo
yaml-tempfile
.DS_Store
**/.DS_Store

11
.prettierrc Normal file
View File

@ -0,0 +1,11 @@
{
"semi": true,
"tabWidth": 3,
"useTabs": false,
"singleQuote": true,
"quoteProps": "as-needed",
"printWidth": 120,
"trailingComma": "all",
"endOfLine": "lf",
"vueIndentScriptAndStyle": false
}

25
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,25 @@
{
//
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
"*.ts": "$(capture).test.ts, $(capture).test.tsx",
"*.tsx": "$(capture).test.ts, $(capture).test.tsx",
"package.json": "index.html,pnpm-lock.yaml,pnpm-workspace.yaml,LICENSE,.gitignore,.gitattributes,.gitpod.yml,CNAME,README*,.npmrc,.browserslistrc,.env.*,.eslintrc.js,.prettierrc,env.d.ts",
"vite.config.ts": "tsconfig.*.json,postcss.config.ts,tailwind.config.ts,tsconfig.json"
},
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "always"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

105
README.md Normal file
View File

@ -0,0 +1,105 @@
# 502404_ManageDb
### 系统管理平台
基础后台管理,无组织管理,包含功能:
#### 基础配置
- 1、启动配置
- 2、类别管理
- 3、行政管理
#### 客户管理
- 4、项目管理
- 5、用户管理
- 6、用户组管理 TODO
- 7、权限管理(基于用户组) TODO
#### 公共管理
- 9、任务管理
- 10、任务日志
- 11、天气查询
- 12、登录日志
### 安装使用步骤
- **Install**
```text
pnpm install
```
- **Run**
```text
pnpm dev
```
- **Build**
```text
pnpm build
```
- **preview**
```text
pnpm preview
```
### 文件资源目录 📚
```text
502402_BasicManage
├─ .vscode # VSCode 推荐配置
├─ mock # Mock模拟数据
├─ public # 静态资源文件
│ ├─ assets # 静态资源文件
├─ src
│ ├─ api # API 接口管理
│ │ └─ typings # 接口 ts 声明
│ ├─ assets # 静态资源文件
│ │ └─ styles # 全局样式文件
│ ├─ const # 全局 常量 声明(颜色/长宽)
│ ├─ components # 全局组件
│ ├─ router # 路由管理
│ ├─ stores # pinia store
│ ├─ utils # 常用工具库
│ ├─ views # 项目所有页面
│ ├─ App.vue # 项目主组件
│ ├─ main.ts # 项目入口文件
│ └─ vite-env.d.ts # 指定 ts 识别 vue
├─ .env # vite 常用配置
├─ .env.development # 开发环境配置
├─ .env.production # 生产环境配置
├─ .env.test # 测试环境配置
├─ .prettierrc # vs代码规范文件
├─ index.html # 入口 html
├─ pnpm-lock.yaml # 依赖包包版本锁
├─ package.json # 依赖包管理
├─ README.md # README 介绍
├─ tsconfig.json # typescript 全局配置
└─ vite.config.ts # vite 全局配置文件
```
### 开发环境 🔨
- 使用 Vue3.3 + TypeScript + Vite4 + Pinia + VueRouter开发
- UI组件: ant-design-vue
- CSS预处理器: sass
- vite插件: axios
- Node环境: v18.16.0
### Git commit ⻛格指南
- feat: 增加新功能
- fix: 修复问题
- style: 代码⻛格相关⽆影响运⾏结果的
- perf: 优化/性能提升
- refactor: 重构
- revert: 撤销修改
- test: 测试相关
- docs: ⽂档/注释
- chore: 依赖更新/脚⼿架配置修改等
- ci: 持续集成

11
env.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
declare module '*.vue' {
import { ComponentOptions } from 'vue';
const componentOptions: ComponentOptions;
export default componentOptions;
}
interface ImportMetaEnv {}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

19
index.html Normal file
View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="null">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>后台管理系统</title>
<style>
html {
overflow-y: hidden;
overflow-x: hidden;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

51
package.json Normal file
View File

@ -0,0 +1,51 @@
{
"name": "basic-manager",
"private": true,
"version": "0.1.0",
"main": "main.ts",
"type": "module",
"scripts": {
"dev": "vite --host",
"gen": "yaml-tempfile",
"build": "vue-tsc -b && vite build --mode production",
"build:test": "vue-tsc && vite build --mode test",
"build:pro": "vue-tsc && vite build --mode production",
"preview": "vite preview"
},
"dependencies": {
"ace-builds": "^1.37.1",
"ant-design-vue": "^4.2.6",
"dayjs": "^1.11.13",
"pinia": "^2.3.0",
"pinia-plugin-persistedstate": "^4.2.0",
"vue": "^3.5.13",
"vue-draggable-next": "^2.2.1",
"vue-m-message": "^4.0.2",
"vue-router": "^4.5.0",
"vue3-ace-editor": "^2.2.4"
},
"devDependencies": {
"@skyfox2000/vite-plugin-auto-import-dts": "^1.0.3",
"@types/mockjs": "1.0.7",
"@types/node": "^22.10.2",
"@vitejs/plugin-vue": "^5.2.1",
"autoprefixer": "^10.4.20",
"esbuild": "^0.19.12",
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-vue": "^9.32.0",
"mockjs": "^1.1.0",
"postcss": "^8.4.49",
"prettier": "^3.4.2",
"rollup": "^4.29.1",
"rollup-plugin-gzip": "^4.0.1",
"rollup-plugin-visualizer": "^5.13.1",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"vite": "^6.0.7",
"vite-plugin-mock": "3.0.1",
"vite-svg-loader": "^5.1.0",
"vue-tsc": "^2.2.0"
}
}

View File

@ -0,0 +1,83 @@
import { Plugin } from 'vite';
import fs from 'fs';
import path from 'path';
/**
*
*/
function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
/**
* .vue
*/
function scanDir(dir: string, baseDir: string): string[] {
const result: string[] = [];
const files = fs.readdirSync(dir);
files.forEach((file) => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
// 递归扫描子目录
result.push(...scanDir(filePath, baseDir));
} else if (file.endsWith('.vue')) {
const relativePath = path.relative(baseDir, filePath);
const componentName = generateComponentName(filePath, baseDir);
result.push(
`export { default as ${componentName} } from './${relativePath.replace(
/\\/g,
'/',
)}';`,
);
}
});
return result;
}
/**
*
*/
function generateComponentName(filePath: string, baseDir: string): string {
const dirName = path.basename(path.dirname(filePath));
const fileName = path.basename(filePath, '.vue');
if (fileName === 'index') {
return capitalize(dirName);
} else {
return capitalize(fileName);
}
}
/**
*
*/
export default function AutoGenerateVue(options: {
dir: string;
output: string;
}): Plugin {
return {
name: 'vite-plugin-auto-generate-vue',
config() {
const { dir, output } = options;
const componentsDir = path.resolve(process.cwd(), dir);
const outputPath = path.resolve(process.cwd(), output);
if (!fs.existsSync(componentsDir)) {
throw new Error(
`The specified directory "${componentsDir}" does not exist.`,
);
}
const exportStatements = scanDir(componentsDir, componentsDir).join('\n');
// 写入到目标文件
fs.writeFileSync(outputPath, exportStatements, 'utf-8');
console.log(`[autoGenerateVue] Global components written to ${output}`);
},
};
}

6
postcss.config.ts Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

69
public/layout/appicons.js Normal file

File diff suppressed because one or more lines are too long

5
src/App.vue Normal file
View File

@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template>
<router-view></router-view>
</template>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -0,0 +1,23 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body,
#app {
padding: 0;
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
@keyframes rotate {
0% {
transform: rotate(0deg)
}
100% {
transform: rotate(360deg)
}
}

View File

@ -0,0 +1,112 @@
<script setup lang="ts">
/**
* 按钮组件
* @component
* @name Button
* @summary 提供一个可点击的图标按钮支持自动切换图标发送点击事件等功能
*/
import { Button } from 'ant-design-vue';
import Tooltip from '../tooltip/index.vue';
import LayoutIcon from '../icon/layoutIcon.vue';
const props = defineProps({
/**
* 提示标题
* @props
* @name content
* @type {string}
*/
tiptext: String,
/**
* 提示显示位置
* @props
* @name placement
* @type {string}
* @default 'top'
*/
placement: {
type: String,
default: 'top',
},
/**
* 内置图标属性设置
* @props
* @name iconProps
* @type {object}
* @summary 内置图标复杂属性设置仅支持前置图标
*/
iconProps: Object,
/**
* 默认使用框架图标
* 其它图标请自定义
* @props
* @name icon
* @type {string}
*/
icon: String,
// /**
// *
// * @props
// * @name clickEvent
// * @summary "#"#.
// * @type {string}
// */
// clickEvent: {
// type: String
// },
// /**
// *
// * @props
// * @name data
// * @summary
// * @type {object|string}
// */
// data: {
// type: [Object, String]
// }
});
//
const emits = defineEmits([
/**
* 点击事件
* @emits
* @name click
* @summary 图标按钮点击时触发的事件
*/
'click',
]);
const onClicked = () => {
// if (props.clickEvent) {
// const eventNames = props.clickEvent.split("#");
// if (eventNames.length === 2) {
// const $Bus = inject("$" + eventNames[0]) as any;
// $Bus.$emit(eventNames[1], props.data);
// }
// }
if (props.iconProps && props.iconProps.icons) {
props.iconProps.iconIndex = (props.iconProps.iconIndex + 1) % props.iconProps.icons.length;
}
emits('click');
};
</script>
<template>
<Tooltip :title="props.tiptext" :disabled="props.tiptext ? undefined : 'disabled'" :placement="placement">
<Button class="px-[10px] py-[4px] flex items-center gap-1" v-bind="$attrs" @click="onClicked">
<template #icon>
<slot name="icon">
<LayoutIcon
:icon="props.icon"
:class="[$attrs.type === 'primary' ? 'ant-btn-primary' : '', 'cursor-pointer w-[17px] h-[17px] mx-auto']"
v-bind="props.iconProps"
/>
</slot>
</template>
<template #default>
<slot></slot>
</template>
</Button>
</Tooltip>
</template>

View File

@ -0,0 +1,27 @@
<script lang="ts" setup>
import { SERVER_HOST } from '@skyfox2000/fapi';
import { createFromIconfont } from '@skyfox2000/webbase';
/**
* 应用图标加载
* @returns 应用图标集
*/
const AppIcon = () => {
return createFromIconfont({
iconUrl: `${SERVER_HOST.APP_ICONS}`,
});
};
/**
* 加载全部应用图标
*/
const AppIcons = AppIcon()
defineProps({
icon: {
type: String
}
});
</script>
<template>
<AppIcons v-if="icon" :icon="icon" :class="['w-6', 'h-6', 'text-2xl', 'align-middle', 'mx-auto']" v-bind="$attrs"/>
</template>

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import { onFullscreenClick, useSettingInfo } from '@skyfox2000/webbase';
import LayoutIcon from './layoutIcon.vue';
const settingStore = useSettingInfo();
</script>
<template>
<LayoutIcon
@click.stop="onFullscreenClick"
:icon="settingStore.fullscreen ? 'icon-exitscreen' : 'icon-fullscreen'"
class="w-[17px] h-[17px]"
/>
</template>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import { Popover } from 'ant-design-vue';
import LayoutIcon from './layoutIcon.vue';
defineProps<{
text?: string,
maxWidth?: string;
}>();
</script>
<template>
<Popover placement="topRight" v-bind="$attrs">
<template #content>
<slot>
<div class="text-[14px]" :style="{ width: maxWidth ? maxWidth : '250px' }">
{{ text }}
</div>
</slot>
</template>
<span class="ml-2">
<LayoutIcon icon="icon-question-circle" class="text-[#888]" v-bind="$attrs"/>
</span>
</Popover>
</template>

View File

@ -0,0 +1,421 @@
<script setup lang="ts">
/**
* 图标组件
* @component
* @name Icon
* @summary 提供一个可点击的图标按钮支持自动切换图标发送点击事件等功能
*/
import { ref, computed, inject, watch } from 'vue';
import Tooltip from '../tooltip/index.vue';
import { getIconTransform } from '@skyfox2000/webbase';
const props = defineProps({
/**
* 点击后自动切换
* @props
* @name autoSwitch
* @type {boolean}
* @default true
*/
autoSwitch: {
type: Boolean,
default: true,
},
/**
* 提示标题
* @props
* @name tiptext
* @type {string}
*/
tiptext: {
type: String,
},
/**
* 提示尺寸
* @props
* @name tipsize
* @type {string}
*/
tipsize: {
type: String,
},
/**
* 提示背景色
* @props
* @name tipcolor
* @type {string}
*/
tipcolor: {
type: String,
},
/**
* 提示显示位置
* @props
* @name placement
* @type {string}
* @default 'top'
*/
placement: {
type: String,
default: 'top',
},
/**
* IconFont图标
* @props
* @name icon
* @type {string}
* @summary IconFont图标显示使用sym-开头的使用svg模式显示
*/
icon: {
type: String,
},
/**
* IconFont图标组
* @props
* @name icons
* @type {Array<string>}
* @summary IconFont图标组显示使用sym-开头的使用svg模式显示
* @default []
*/
icons: {
type: Array<string>,
default: () => [],
},
/**
* 图标索引
* @props
* @name iconIndex
* @type {number}
* @default 0
*/
iconIndex: {
type: Number,
default: 0,
},
/**
* 可点击鼠标手
* @props
* @name clickable
* @type {boolean}
* @default false
*/
clickable: {
type: Boolean,
default: false,
},
/**
* 点击事件
* @props
* @name clickEvent
* @summary 格式 "空间名#事件名"空间名和事件名用#分隔事件名用.分隔
* @type {string}
*/
clickEvent: {
type: String,
},
/**
* 点击传输数据
* @props
* @name data
* @summary 点击事件传输的默认数据
* @type {object|string}
*/
data: {
type: [Object, String],
},
/**
* 字体大小
* @props
* @name size
* @summary 字体大小
* @type {string}
* @default 20px
*/
fontsize: {
type: String, //
default: '20px',
},
/**
* 空间大小
* @props
* @name size
* @summary 空间大小
* @type {string, [string, string]}
* @default 20px
*/
size: {
type: [String, Array<String>], //
default: () => ['20px', '20px'],
},
/**
* 图标位置
* @props
* @name position
* @summary 图标位置
* @type {[string|number, string|number]}
*/
position: {
type: Array<string | number>, //
},
/**
* 旋转中心
* @props
* @name center
* @summary 旋转中心
* @type {string}
*/
center: {
type: String, //
},
/**
* 指定角度
* @props
* @name angle
* @summary 指定角度
* @type {number}
*/
angle: {
type: Number, //
},
/**
* 颜色
* @props
* @name color
* @summary 颜色
* @type {string}
*/
color: {
type: String, //
default: '',
},
/**
* 样式
* @props
* @name hovercolor
* @summary 样式
* @type {string}
*/
className: {
type: String, //
default: '',
},
/**
* 水平翻转
* @props
* @name flip
* @summary 水平翻转
* @type {boolean}
* @default false
*/
flip: {
type: Boolean, //
default: false,
},
/**
* 自动旋转
* @props
* @name spin
* @summary 自动旋转
* @type {boolean}
* @default false
*/
spin: {
type: Boolean, //
default: false,
},
});
//
const emits = defineEmits([
/**
* 点击事件
* @emits
* @name click
* @summary 图标按钮点击时触发的事件
*/
'click',
/**
* 图标索引更新
* @emits
* @name update:iconIndex
* @summary 图标索引发生变化时触发的事件
* @param {number} index - 新的图标索引
*/
'update:iconIndex',
]);
const boxSize = computed(() => {
if (Array.isArray(props.size)) return props.size;
else {
let size = props.size;
return [size, size];
}
});
const curIcon = ref(props.icon);
const curIndex = ref(props.iconIndex);
watch(
() => props.icon,
(newVal) => {
curIcon.value = newVal;
},
);
watch(
() => props.iconIndex,
(newVal) => {
curIndex.value = newVal;
curIcon.value = props.icons[newVal];
},
);
const iconIndex = computed<number>({
get() {
return curIndex.value;
},
set(val) {
curIndex.value = val;
emits('update:iconIndex', curIndex.value);
},
});
if (props.icons.length > 0) {
iconIndex.value = iconIndex.value >= props.icons.length ? 0 : iconIndex.value;
curIcon.value = props.icons[curIndex.value];
} else {
curIcon.value = props.icon;
}
const onClicked = (e: MouseEvent) => {
e.stopPropagation();
if (props.clickEvent) {
const eventNames = props.clickEvent.split('#');
if (eventNames.length === 2) {
const $Bus = inject('$' + eventNames[0]) as any;
$Bus.$emit(eventNames[1], props.data);
}
}
if (props.autoSwitch && props.icons.length > 0) {
iconIndex.value = (iconIndex.value + 1) % props.icons.length;
curIcon.value = props.icons[curIndex.value];
}
emits('click');
};
const getIconClass = (): string => {
let className = '';
if (props.spin) {
className += 'rotate';
}
if (props.flip) {
className += ' flip';
}
return className;
};
</script>
<template>
<Tooltip
:title="props.tiptext"
:disabled="props.tiptext ? undefined : 'disabled'"
:color="tipcolor"
:placement="placement"
:size="tipsize"
>
<div
class="re-icon-container"
v-if="curIcon"
:style="{
width: boxSize[0].toString(),
height: boxSize[1].toString(),
}"
>
<!--
使用动态 class 绑定通过 icon-class 控制图标样式curIcon 是计算属性
可以根据 iconIndex 控制图标显示点击图标时执行 onClicked 方法
-->
<i
v-bind="$attrs"
v-if="!curIcon?.startsWith('sym-')"
class="re-icon iconfont fontclass"
:class="[props.clickable ? 'clickable' : '', 'icon-' + curIcon, getIconClass(), props.className]"
:style="{
top: props.position ? props.position[1] : 1,
left: props.position ? props.position[0] : 0,
fontSize: props.fontsize,
transformOrigin: props.center ?? 'center center',
transform: getIconTransform(props.angle, props.flip),
color: props.color,
}"
aria-hidden="true"
@click="onClicked"
></i>
<svg
v-else
v-bind="$attrs"
class="re-icon symbol"
aria-hidden="true"
:class="[props.clickable ? 'clickable' : '', getIconClass(), props.className]"
:style="{
top: props.position ? props.position[1] : 0,
left: props.position ? props.position[0] : 0,
fontSize: props.fontsize,
transformOrigin: props.center ?? 'center center',
transform: getIconTransform(),
color: props.color,
}"
>
<use :xlink:href="'#icon-' + curIcon.replace('sym-', '')"></use>
</svg>
</div>
</Tooltip>
</template>
<style lang="less" scoped>
.re-icon-container {
position: relative;
display: inline-flex;
vertical-align: middle;
justify-content: center;
align-items: center;
overflow: hidden;
// margin-right: 3px;
}
.re-icon.symbol {
position: relative;
margin: 5px 0;
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.re-icon.fontclass {
position: relative;
font-style: normal;
-webkit-font-smoothing: antialiased;
-webkit-text-stroke-width: 0.2px;
-moz-osx-font-smoothing: grayscale;
}
.re-icon[disabled] {
pointer-events: none;
cursor: not-allowed;
color: #666;
}
.clickable {
cursor: pointer;
}
.rotate {
animation: rotate 2s linear infinite;
}
.flip {
transform: scaleX(-1);
}
</style>

View File

@ -0,0 +1,31 @@
<script lang="ts" setup>
import { SERVER_HOST } from '@skyfox2000/fapi';
import { createFromIconfont } from '@skyfox2000/webbase';
/**
* 基座图标加载
* @returns 基座图标集
*/
const LayoutIcon = () => {
return createFromIconfont({
iconUrl: `${SERVER_HOST.MICROLAYOUT_ICONS}`,
monoColor: true,
});
};
/**
* 基座图标集
*/
const LayoutIcons = LayoutIcon();
defineProps({
icon: {
type: String,
},
icons: {
type: Array<string>,
},
});
</script>
<template>
<LayoutIcons v-if="icon || icons" :icon="icon" :icons="icons" v-bind="$attrs" />
</template>

View File

@ -0,0 +1,31 @@
<script lang="ts" setup>
import { SERVER_HOST } from '@skyfox2000/fapi';
import { createFromIconfont } from '@skyfox2000/webbase';
/**
* 表格工具图标加载
* @returns 表格工具图标加载
*/
const ToolIcon = () => {
return createFromIconfont({
iconUrl: `${SERVER_HOST.TOOL_ICONS}`,
monoColor: true
});
};
/**
* 表格工具图标集
*/
const ToolIcons = ToolIcon();
defineProps({
icon: {
type: String
},
icons: {
type: Array<string>,
},
});
</script>
<template>
<ToolIcons v-if="icon || icons" :icon="icon" :icons="icons" v-bind="$attrs" />
</template>

View File

@ -0,0 +1,12 @@
import Tooltip from './tooltip/index.vue';
export { Tooltip };
import Icon from './icon/index.vue';
export { Icon };
import Fullscreen from './icon/fullscreen.vue';
export { Fullscreen };
import Helper from './icon/helper.vue';
export { Helper };
import ToolIcon from './icon/toolIcon.vue';
export { ToolIcon };
import Button from './button/index.vue';
export { Button };

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
import { CSSProperties } from 'vue';
import { Tooltip } from 'ant-design-vue';
const props = defineProps<{
size?: string;
}>();
const overlayStyle: CSSProperties = {
}
const overlayInnerStyle: CSSProperties = {
}
if (props.size === 'small') {
overlayInnerStyle.fontSize = '12px';
overlayInnerStyle.padding = '4px 6px';
overlayInnerStyle.minHeight = '26px';
overlayStyle.height = '30px';
}
</script>
<template>
<Tooltip :overlayInnerStyle="overlayInnerStyle" :overlayStyle="overlayStyle">
<slot />
</Tooltip>
</template>

View File

@ -0,0 +1,119 @@
<script setup lang="ts">
import { watch, ref, onMounted } from 'vue';
import { Button } from '../../common';
import { Modal, Space } from 'ant-design-vue';
import { PageData, onFormSave, onFormSaveAs, onFormClose } from '@skyfox2000/webbase';
import { AnyData } from '@skyfox2000/fapi';
const props = defineProps<{
/**
* 确认按钮文字空字符串则不显示
*/
saveText?: string;
/**
* 另存为按钮文字空字符串则不显示
*/
saveAsText?: string;
/**
* 取消按钮文字空字符则不显示
*/
cancelText?: string;
/**
* 保存数据请求配置
*/
pageData?: PageData<AnyData>;
/**
* 全屏模式
*/
full?: boolean;
}>();
const editorData = props.pageData?.editor!;
const open = ref<boolean>(false);
watch(
() => editorData.visible,
(newVal) => {
open.value = newVal;
},
);
onMounted(() => {
open.value = editorData.visible;
});
const dialogSave = () => {
if (props.pageData) onFormSave(props.pageData);
};
const dialogSaveAs = () => {
if (props.pageData) onFormSaveAs(props.pageData);
};
const dialogClose = () => {
if (props.pageData) onFormClose(props.pageData);
else open.value = false;
};
</script>
<template>
<Modal
v-model:open="open"
:wrapClassName="
'modal mx-auto ' +
($attrs.width ? 'w-[' + $attrs.width + ']' : 'w-[430px]') +
' ' +
(full ? 'full-modal w-full' : '')
"
@close="dialogClose"
>
<slot></slot>
<template #footer>
<Space>
<Button @click="dialogClose" v-if="cancelText !== ''">
{{ cancelText ?? '取消' }}
</Button>
<Button
@click="dialogSaveAs"
v-if="saveAsText !== '' && editorData.saveAsBtnVisible !== false"
type="primary"
:loading="editorData.isFormSaving"
>
{{ saveAsText ?? '另存为' }}
</Button>
<Button
@click="dialogSave"
v-if="saveText !== '' && editorData.saveBtnVisible !== false"
type="primary"
:loading="editorData.isFormSaving"
>
{{ saveText ?? '保存' }}
</Button>
</Space>
</template>
</Modal>
</template>
<style>
.modal {
.ant-modal-content {
padding: 16px;
}
}
.full-modal {
.ant-modal {
width: 100% !important;
max-width: 100%;
top: 0;
padding-bottom: 0;
margin: 0;
}
.ant-modal-content {
display: flex;
flex-direction: column;
height: calc(100vh);
}
.ant-modal-body {
flex: 1;
}
}
</style>

View File

@ -0,0 +1,144 @@
<script setup lang="ts">
import { watch, ref, onMounted } from 'vue';
import { Button } from '../../common';
import { Modal, Space } from 'ant-design-vue';
import { AsyncUploader, EditorData, PageData } from '@skyfox2000/webbase';
import { AnyData, IUrlInfo } from '@skyfox2000/fapi';
import UploadFileList from './uploadList.vue';
import { UploadFile } from '@skyfox2000/webbase';
import message from 'vue-m-message';
const props = defineProps<{
/**
* 文件后缀限制
*/
fileExt?: string[];
/**
* 保存数据请求配置
*/
pageData: PageData<AnyData>;
/**
* 最大文件数
* 默认1个
*/
maxCount?: number;
/**
* 并发上传文件数
* 默认3个
*/
maxConcurrent?: number;
/**
* 控制弹窗
*/
uploadForm: EditorData<any>;
/**
* 上传地址和参数
*/
url?: IUrlInfo;
}>();
const uploaderForm = ref(props.uploadForm);
const open = ref<boolean>(false);
const maxCount = props.maxCount ?? 1;
const maxConcurrent = props.maxConcurrent ?? 3;
const fileList = ref<UploadFile[]>([]);
watch(
() => uploaderForm.value.visible,
() => {
open.value = uploaderForm.value.visible;
},
);
const dialogUpload = async () => {
const url = props.url ?? props.pageData?.urls.upload;
if (!url) {
message.error('未配置文件上传地址!');
return;
}
if (!url.api) url.api = props.pageData.api;
if (url.authorize === undefined) url.authorize = props.pageData.authorize;
const uploader = new AsyncUploader(url, maxConcurrent);
uploaderForm.value.isFormLoading = true;
try {
if (fileList.value.length === 0) {
message.warning('请选择上传的文件!');
setTimeout(() => {
uploaderForm.value.isFormLoading = false;
}, 10000);
return;
}
//
await uploader.upload(
fileList.value,
(file) => {
console.log(`${file.name} 上传进度:${file.percent}% (${file.status})`);
},
(files) => {
uploaderForm.value.isFormLoading = false;
console.log('所有文件上传结束:', files);
},
);
} catch (error) {
uploaderForm.value.isFormLoading = false;
console.error('上传错误:', error);
}
};
onMounted(() => {
open.value = uploaderForm.value.visible;
});
const dialogClose = () => {
uploaderForm.value.visible = false;
};
</script>
<template>
<Modal
title="文件上传"
v-model:open="open"
:wrapClassName="'modal mx-auto ' + ($attrs.width ? 'w-[' + $attrs.width + ']' : 'w-[430px]')"
@close="dialogClose"
>
<UploadFileList v-model:file-list="fileList" :max-count="maxCount" :file-ext="fileExt" />
<template #footer>
<Space>
<Button @click="dialogClose">取消</Button>
<Button @click="dialogUpload" type="primary" :loading="uploaderForm.isFormSaving"> 上传文件 </Button>
</Space>
</template>
</Modal>
</template>
<style>
.modal {
.ant-modal-content {
padding: 16px;
}
}
.full-modal {
.ant-modal {
width: 100% !important;
max-width: 100%;
top: 0;
padding-bottom: 0;
margin: 0;
}
.ant-modal-content {
display: flex;
flex-direction: column;
height: calc(100vh);
}
.ant-modal-body {
flex: 1;
}
}
</style>

View File

@ -0,0 +1,150 @@
<script setup lang="ts">
import { Button } from '@/components';
import { computed, ref } from 'vue';
import message from 'vue-m-message';
import type { UploadProps } from 'ant-design-vue';
import { Upload, Progress, Tag } from 'ant-design-vue';
import { UploadFile, UploadFileStatus } from '@skyfox2000/webbase';
import { watch } from 'vue';
interface Props {
fileList: UploadFile<any>[];
placeholder?: string;
fileExt?: string[];
maxFileSize?: number;
maxCount?: number;
autoUpload?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
fileList: () => [],
placeholder: '',
maxFileSize: 20,
maxCount: 5,
autoUpload: false,
});
const fileList = ref<UploadFile[]>([]);
const fileUploader = ref();
const emit = defineEmits(['update:file-list']);
const acceptString = computed(() => (props.fileExt?.length ? props.fileExt.map((ext) => `.${ext}`).join(',') : ''));
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
if (props.fileExt && props.fileExt.length > 0) {
const extension = file.name.split('.').pop()?.toLowerCase() || '';
if (!props.fileExt.includes(extension)) {
message.error('文件类型不支持');
return false;
}
}
if (file.size / 1024 / 1024 > props.maxFileSize) {
message.error(`文件大小超过 ${props.maxFileSize}MB 限制`);
return false;
}
if (fileList.value.length >= props.maxCount) {
message.error(`最多上传 ${props.maxCount} 个文件`);
return false;
}
return props.autoUpload;
};
const uploadProps = computed<UploadProps>(() => ({
accept: acceptString.value,
multiple: true,
fileList: fileList.value as UploadProps['fileList'],
beforeUpload: beforeUpload,
listType: 'text',
maxCount: props.maxCount,
showUploadList: false,
onChange: (info) => {
if (!props.autoUpload) {
fileList.value = info.fileList as unknown as UploadFile[];
}
},
}));
watch(
() => fileList.value,
(newVal) => {
emit('update:file-list', newVal);
},
{ deep: true },
);
const getPlaceholder = (): string => {
const typeMsg = props.fileExt && props.fileExt.length ? `文件必须为 ${props.fileExt.join('/')}` : '';
const sizeMsg = props.maxFileSize !== 0 ? `单文件最大 ${props.maxFileSize}MB` : '';
const countMsg = props.maxCount !== 0 ? `最多 ${props.maxCount} 个文件` : '';
return [sizeMsg, typeMsg, countMsg].filter(Boolean).join('');
};
const removeFile = (index: number) => {
fileList.value.splice(index, 1);
};
const getStatusColor = (status?: UploadFileStatus) => {
switch (status) {
case UploadFileStatus.Uploading:
return 'blue';
case UploadFileStatus.Success:
return 'green';
case UploadFileStatus.Error:
return 'red';
default:
return 'cyan';
}
};
const getStatus = (status?: UploadFileStatus) => {
switch (status) {
case UploadFileStatus.Uploading:
return '上传中';
case UploadFileStatus.Success:
return '已完成';
case UploadFileStatus.Error:
return '上传失败';
default:
return '待上传';
}
};
</script>
<template>
<div class="w-full border border-solid border-gray-100 mt-1 rounded-md py-5">
<div class="flex items-center justify-between w-full">
<div class="w-35 mx-3">
<Upload ref="fileUploader" v-bind="uploadProps">
<Button>选择文件</Button>
</Upload>
</div>
<div class="flex-1 text-sm text-gray-500">{{ getPlaceholder() }}</div>
<!-- <Button v-if="!autoUpload" @click="manualUpload" class="mr-3">开始上传</Button> -->
</div>
<div class="mt-4 px-3">
<div v-for="(file, index) in fileList" :key="index" class="mb-2 pb-1">
<div class="flex items-center justify-between">
<div class="flex items-center">
<span class="text-gray-700 mr-2">{{ file.name }}</span>
<span>
<Tag :color="getStatusColor(file.status)">{{ getStatus(file.status) }}</Tag>
</span>
</div>
<div class="flex items-center">
<!-- <span class="text-blue-500 hover:text-blue-700 mr-4">预览</span> -->
<span class="text-red-500 hover:text-red-700 cursor-pointer" @click="removeFile(index)">删除</span>
</div>
</div>
<!-- 上传进度条 -->
<div>
<Progress :percent="file.percent" :stroke-width="2" :show-info="false" style="height: 2px" />
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,110 @@
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
import { Button } from '../../common';
import { Drawer, Space, theme } from 'ant-design-vue';
const { useToken } = theme;
const { token } = useToken();
import { PageData, onFormSave, onFormSaveAs, onFormClose } from '@skyfox2000/webbase';
import { AnyData } from '@skyfox2000/fapi';
import LayoutIcon from '@/components/common/icon/layoutIcon.vue';
const open = ref<boolean>(false);
// interface RuleItem {
// key: string[]; // 使
// validate: (value: any) => boolean; //
// message: string
// }
const props = defineProps<{
/**
* 确认按钮文字空字符串则不显示
*/
saveText?: string;
/**
* 另存为按钮文字空字符串则不显示
*/
saveAsText?: string;
/**
* 取消按钮文字空字符则不显示
*/
cancelText?: string;
/**
* 保存数据请求配置
*/
pageData: PageData<AnyData>;
}>();
const editorData = props.pageData.editor!;
watch(
() => editorData.visible,
() => {
open.value = editorData.visible;
},
);
onMounted(() => {
open.value = props.pageData.editor!.visible;
});
</script>
<template>
<Drawer
v-model:open="open"
:get-container="false"
:closable="false"
:header-style="{
height: '40px',
padding: '10px 6px 10px 16px',
backgroundColor: token.colorBgLayout,
}"
:body-style="{
padding: '10px 16px',
}"
:footer-style="{
textAlign: 'right',
}"
:style="{
position: 'absolute',
boxShadow: 'rgba(0, 0, 0, 0.3) -2px 0px 8px',
}"
width="412px"
@close="() => onFormClose(pageData)"
>
<template #extra>
<div class="hover:bg-gray-200 w-[24px] h-[24px] rounded-md">
<LayoutIcon
class="top-[-2px] left-[2px]"
icon="add"
:angle="45"
fontsize="24px"
clickable
color="#666"
:position="[0, 0]"
@click="() => onFormClose(pageData)"
/>
</div>
</template>
<template #default>
<slot></slot>
</template>
<template #footer>
<Space>
<Button @click="() => onFormClose(pageData)" v-if="cancelText !== ''">{{ cancelText ?? '取消' }}</Button>
<Button
@click="() => onFormSaveAs(pageData)"
v-if="saveAsText !== '' && editorData.saveAsBtnVisible !== false"
type="primary"
:loading="editorData.isFormSaving"
>{{ saveAsText ?? '另存为' }}</Button
>
<Button
@click="() => onFormSave(pageData)"
v-if="saveText !== '' && editorData.saveBtnVisible !== false"
type="primary"
:loading="editorData.isFormSaving"
>{{ saveText ?? '保存' }}</Button
>
</Space>
</template>
</Drawer>
</template>

View File

@ -0,0 +1,54 @@
<script setup lang="ts">
import { useFormItemFactory } from '@skyfox2000/webbase';
import { FormItem } from 'ant-design-vue';
import { Helper } from '@/components/common';
const props = defineProps<{
/**
* 标签文字
*/
label?: string;
/**
* 验证规则
*/
rule?: string;
/**
* 帮助文字
*/
helper?: string;
}>();
const { msg } = useFormItemFactory({
label: props.label,
rule: props.rule,
});
//
// this.$parent.$el.scrollIntoView({
// //
// block: "center", //start,center,endnearest
// behavior: "smooth" //autoinstant,smooth
// });
</script>
<template>
<div class="w-full relative mb-1">
<FormItem :required="rule !== undefined" class="!w-[95%] relative" v-bind="$attrs" :class="[rule ? '' : 'mb-3']">
<template #label>
<span :class="[msg ? 'text-[#ff4d4f]' : '', 'w-full']"> {{ label }}</span>
</template>
<div class="w-full flex items-center">
<div class="flex-grow">
<slot></slot>
</div>
<div class="w-8 mt-[-2px]">
<slot name="helper">
<Helper v-if="helper" :text="helper" />
</slot>
</div>
</div>
</FormItem>
<span class="absolute bottom-[3px] left-[80px] text-[12px] text-[#ff4d4fcc] block" v-if="msg">
{{ msg }}
</span>
</div>
</template>

View File

@ -0,0 +1,43 @@
<script lang="ts" setup>
import { EditorData, PageData, ProviderKeys } from '@skyfox2000/webbase';
import { AnyData } from '@skyfox2000/fapi';
import { Form } from 'ant-design-vue';
import { inject, provide, ref } from 'vue';
// 使
// :label-col="{ flex: '60px' }"
// :wrapper-col="{ flex: '200px' }"
// 使FormItem
const props = defineProps<{
/**
* 快速配置表单标签宽度
*/
labelWidth?: string;
/**
* 快速配置表单项宽度
*/
wrapperWidth?: string;
/**
* 表单配置信息
*/
editorData?: EditorData<AnyData>;
}>();
const pageData = ref<PageData<AnyData> | undefined>(inject(ProviderKeys.PageData, undefined));
const editorData = props.editorData ?? (pageData.value ? pageData.value.editor : undefined);
provide(ProviderKeys.EditorData, editorData);
</script>
<template>
<Form
:label-col="{ flex: props.labelWidth ?? '85px' }"
:wrapper-col="{
flex: props.wrapperWidth ?? '1',
}"
:style="{
display: 'flex',
flexWrap: 'wrap',
}"
>
<slot></slot>
</Form>
</template>

View File

@ -0,0 +1,117 @@
<script lang="ts" setup>
import { onMounted, ref, watch, useSlots, VNode } from 'vue';
import { Form, Space } from 'ant-design-vue';
import SearchItem from './searchItem.vue';
import { Button } from '../../common';
import { PageData } from '@skyfox2000/webbase';
import { AnyData } from '@skyfox2000/fapi';
// 使
// :label-col="{ flex: '60px' }"
// :wrapper-col="{ flex: '200px' }"
// 使FormItem
const props = defineProps<{
/**
* 搜索条件
*/
search: Record<string, any>;
/**
* 页面数据
*/
pageData: PageData<AnyData>;
}>();
const emits = defineEmits<{
(e: 'update:search', val: Record<string, any>): void;
}>();
/**
* 搜索按钮前的占位数量1或者2
*/
const holderSize = ref(0);
const defaultSlots = ref(0);
const controlSlots = ref(0);
const getSlotLen = (items: VNode[]): number => {
let count = 0;
for (let i = 0; i < items.length; i++) {
if (typeof items[i].type === 'object') count++;
}
return count;
};
const slots = useSlots();
const updateHolderSize = () => {
defaultSlots.value = 0;
controlSlots.value = 0;
if (slots.default) defaultSlots.value = getSlotLen(slots.default({}));
if (props.pageData.searchBar && slots.control) controlSlots.value = getSlotLen(slots.control({}));
holderSize.value = 2 - ((defaultSlots.value + controlSlots.value) % 3);
};
watch(
() => props.pageData.searchBar,
() => {
updateHolderSize();
},
);
const defaultData: Record<string, any> = JSON.parse(JSON.stringify(props.search));
onMounted(() => {
updateHolderSize();
let search = updateSearchNull();
props.pageData.searchQuery = {
Query: { ...search },
};
});
const onSearch = () => {
let search = updateSearchNull();
props.pageData.searchQuery = {
Query: { ...search },
};
if (props.pageData.grid) props.pageData.grid.reload = true;
};
const updateSearchNull = (): Record<string, any> => {
let search = { ...props.search };
for (const key in search) {
if (search[key] === null) {
search[key] = undefined;
}
}
return search;
};
const onReset = () => {
const data = JSON.parse(JSON.stringify(defaultData));
emits('update:search', data);
};
</script>
<template>
<Form
v-if="defaultSlots + controlSlots > 0"
:label-col="{ flex: '60px' }"
ref="searchForm"
:style="{
flexWrap: 'wrap',
borderBottom: '1px solid #e9e9e9',
}"
class="flex mb-[10px]"
>
<!-- 默认插槽 -->
<slot></slot>
<!-- 受控插槽 -->
<slot name="control" v-if="pageData.searchBar"></slot>
<!-- 表单操作按钮 占位数量 -->
<SearchItem class="w-[33%]" v-if="holderSize >= 1"> </SearchItem>
<SearchItem class="w-[33%]" v-if="holderSize >= 2"> </SearchItem>
<SearchItem
v-if="defaultSlots || pageData.searchBar"
class="w-[33%] flex justify-end text-right pr-2"
:wrapper-col="{ flex: 'auto' }"
>
<Space>
<Button type="primary" @click="onSearch">搜索</Button>
<Button @click="onReset">重置</Button>
</Space>
</SearchItem>
</Form>
</template>

View File

@ -0,0 +1,43 @@
<script setup lang="ts">
import { useFormItemFactory } from '@skyfox2000/webbase';
import { FormItem } from 'ant-design-vue';
const props = defineProps<{
/**
* 标签文字
*/
label?: string;
/**
* 验证规则
*/
rule?: string;
}>();
const { msg } = useFormItemFactory({
label: props.label,
rule: props.rule,
});
</script>
<template>
<div class="w-1/3 relative mb-1">
<FormItem
:required="rule !== undefined"
class="!w-[90%] relative"
v-bind="$attrs"
:class="[rule ? '' : 'mb-3']"
:labelCol="{ span: 6 }"
>
<template #label v-if="label">
<span :class="[msg ? 'text-[#ff4d4f]' : '', 'w-full']"> {{ label }}</span>
</template>
<div class="w-full flex items-center">
<div class="flex-grow">
<slot></slot>
</div>
</div>
</FormItem>
<span class="absolute bottom-[3px] left-[80px] text-[12px] text-[#ff4d4fcc] block" v-if="msg">
{{ msg }}
</span>
</div>
</template>

View File

@ -0,0 +1,153 @@
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import { Table, TablePaginationConfig, TableProps } from 'ant-design-vue';
import {
gridQueryFind,
gridStatusUpdate,
PageData,
PrimaryKey,
useSettingInfo,
OPTIONS,
gridQueryList,
} from '@skyfox2000/webbase';
import TableOperate from './tableOperate.vue';
import Toolbar from '../toolbar/index.vue';
import Switch from '../../form/switch/index.vue';
import { RowOperate } from '@skyfox2000/microbase';
import { AnyData } from '@skyfox2000/fapi';
const props = defineProps<{
/**
* 页面主数据
*/
pageData: PageData<AnyData>;
}>();
const pageData = props.pageData;
const gridData = pageData.grid!;
gridData.pageNo = 1;
gridData.total = 0;
gridData.pageSize = gridData.pageSize ?? 10;
const dataList = ref<Record<string, AnyData>[]>([]);
const pagination = ref<TablePaginationConfig>({
total: 0,
current: 1,
pageSize: gridData.pageSize,
showTotal: (total: number) => {
return `${total} 条记录`;
},
onChange: (page: number, pageSize: number) => {
pagination.value.current = page;
pagination.value.pageSize = pageSize;
gridData.pageSize = pageSize;
gridData.pageNo = page;
gridQueryFind(pageData);
},
});
watch(
() => gridData.tableData,
() => {
dataList.value = gridData.tableData!;
pagination.value.total = gridData.total ?? 0;
},
{ deep: true },
);
watch(
() => gridData.reload,
(newVal) => {
if (newVal) {
gridQueryFind(pageData);
gridData.reload = false;
}
},
);
const columns = ref<Record<string, any>[]>();
const selection = {
onChange: (selectedRowKeys: PrimaryKey[], selectedRows: any[]) => {
gridData.selectKeys = selectedRowKeys;
gridData.selectRows = selectedRows;
},
columnWidth: '30px',
};
const rowSelection = ref<TableProps['rowSelection']>(selection);
watch(
() => gridData.selectable,
(newVal) => {
rowSelection.value = newVal ? selection : undefined;
},
{ immediate: true },
);
watch(
() => gridData.columns,
() => {
columns.value = gridData.columns.filter((column) => column.visible !== false);
},
{ deep: true, immediate: true },
);
onMounted(() => {
const settingInfoStore = useSettingInfo();
if (settingInfoStore.rowOperate === RowOperate.RIGHT) {
const operateColumns: Record<string, any>[] = [];
const dataColumns: Record<string, any>[] = [];
gridData.columns!.forEach((column) => {
if (column['key'] === 'operation') {
operateColumns.push(column);
} else dataColumns.push(column);
});
gridData.columns.splice(0, gridData.columns.length);
gridData.columns.push(...dataColumns);
gridData.columns.push(...operateColumns.reverse());
}
if (gridData.tableData) {
dataList.value = gridData.tableData;
gridData.total = dataList.value.length;
pagination.value.total = gridData.total ?? 0;
} else if (gridData.autoload !== false) {
if (gridData.remotePage) gridQueryFind(pageData);
else gridQueryList(pageData);
}
});
</script>
<template>
<Toolbar :page-data="pageData" />
<Table
class="w-full"
:rowKey="pageData.primaryKey"
:data-source="dataList"
:loading="gridData.isGridLoading"
:columns="columns"
:pagination="pagination"
:row-selection="rowSelection"
:scroll="{ x: 700, y: 1000 }"
:size="gridData.tableSize"
bordered
v-bind="$attrs"
>
<template #bodyCell="bodyCell">
<slot name="bodyCell" :column="bodyCell?.column" :record="bodyCell?.record"></slot>
<template v-if="bodyCell?.column.dataIndex === 'enabled' || bodyCell?.column.dataIndex === 'disabled'">
<Switch
v-model:checked="bodyCell.record.Enabled"
:data="OPTIONS.EnableDisable"
@click="gridStatusUpdate(pageData, bodyCell.record)"
:disabled="bodyCell?.column.dataIndex === 'disabled'"
:class="['w-[58px]']"
:loading="bodyCell?.record.isLoading"
/>
</template>
<template v-if="bodyCell?.column.dataIndex === 'operation'">
<slot name="operate" :record="bodyCell?.record">
<TableOperate :record="bodyCell?.record" :tools="gridData.operates" :page-data="pageData">
</TableOperate>
</slot>
</template>
</template>
</Table>
</template>

View File

@ -0,0 +1,134 @@
<script setup lang="ts" generic="T">
import { reactive, watch } from 'vue';
import {
ButtonTool,
PageData,
getToolGroup,
getToolVisible,
getToolStatus,
onToolClicked,
onGridRowEdit,
onGridRowDelete,
} from '@skyfox2000/webbase';
import { ConfigProvider, Button, Space, DropdownButton, Menu, MenuItem, Popconfirm } from 'ant-design-vue';
const props = defineProps<{
/**
* 数据行记录
*/
record: Record<string, any>;
/**
* 页面数据
*/
pageData: PageData<T>;
}>();
const pageData = props.pageData;
const gridData = pageData.grid!;
///
const defaultOperates: ButtonTool[] = [
{
key: 'Edit',
label: '编辑',
type: 'primary',
visible: true,
disabled: props.record.Enabled == 0,
click: () => onGridRowEdit<T>(pageData, props.record as T),
},
{
key: 'Delete',
label: '删除',
type: 'primary',
visible: true,
disabled: props.record.Enabled == 1,
danger: true,
confirm: true,
confirmText: '是否删除此记录?',
click: () => onGridRowDelete<T>(pageData, props.record as T),
},
];
const { buttons, menus } = getToolGroup(defaultOperates, 0, gridData.operates);
const Menus = reactive(menus);
const Buttons = reactive(buttons);
watch(
() => props.record.Enabled,
() => {
const { buttons, menus } = getToolGroup(defaultOperates, 0, gridData.operates);
Buttons.splice(0, Buttons.length, ...buttons);
Menus.splice(0, Menus.length, ...menus);
},
);
const disabled = (item: ButtonTool) => {
switch (item.key) {
case 'Edit':
item.disabled = props.record.Enabled ? false : true;
break;
case 'Delete':
item.disabled = props.record.Enabled ? true : false;
break;
}
return getToolStatus(item, props.record);
};
</script>
<template>
<ConfigProvider
:theme="{
token: {
fontSize: 13,
},
}"
>
<Space>
<template v-for="item in Buttons" :key="item.key">
<Popconfirm
:disabled="!item.confirm"
cancelText="否"
okText="是"
:title="item.confirmText"
:okButtonProps="{ size: 'small' }"
:cancelButtonProps="{ size: 'small' }"
@confirm="onToolClicked(item, pageData, props.record, true)"
>
<Button
:key="item.key"
:type="item.type ?? 'text'"
:danger="item.danger"
v-if="getToolVisible(item)"
:disabled="disabled(item)"
@click="onToolClicked(item, pageData, props.record)"
size="small"
:style="{
padding: item.type ?? '0px 4px',
}"
>
{{ item.label }}
</Button>
</Popconfirm>
</template>
<ConfigProvider :autoInsertSpaceInButton="false" v-if="record.Enabled == 1">
<DropdownButton v-if="Menus.length > 0" size="small">
更多
<template #overlay>
<Menu>
<template v-for="item in Menus" :key="item.key">
<MenuItem
v-if="getToolVisible(item)"
:disabled="disabled(item)"
@click="onToolClicked(item, pageData, props.record)"
>
{{ item.label }}
</MenuItem>
</template>
</Menu>
</template>
</DropdownButton>
</ConfigProvider>
</Space>
</ConfigProvider>
</template>

View File

@ -0,0 +1,143 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { onColumnVisibleChanged } from '@skyfox2000/webbase';
import { VueDraggableNext as draggable } from 'vue-draggable-next';
import { Checkbox } from 'ant-design-vue';
import {
PageData,
getToolStatus,
getToolByKey,
getToolVisible,
useToolFactory,
onToolClicked,
} from '@skyfox2000/webbase';
import { Button, ToolIcon } from '@/components';
import { Dropdown, Menu, MenuItem } from 'ant-design-vue';
//
// const componentMap: Record<string, any> = {
// headset: () => import('./headset.vue'),
// };
const props = defineProps<{
/**
* 页面主数据
*/
pageData: PageData<any>;
}>();
const gridData = props.pageData.grid!;
const { tools } = useToolFactory(props.pageData);
watch(
() => gridData.selectable,
(newVal) => {
if (getToolByKey(tools.value, 'tool.export.excel.selected'))
getToolByKey(tools.value, 'tool.export.excel.selected')!.disabled = !newVal;
if (getToolByKey(tools.value, 'tool.export.pdf.selected'))
getToolByKey(tools.value, 'tool.export.pdf.selected')!.disabled = !newVal;
},
);
const columns = gridData.columns;
//
const dragColumns = ref(columns);
//
const onDragEnd = () => {
//
columns.splice(0, columns.length, ...dragColumns.value);
};
</script>
<template>
<div class="inline-flex [&>button]:ml-[-1px] first:[&>button]:ml-0">
<template v-for="(item, index) in tools" :key="item.key">
<Dropdown placement="bottomRight" class="p-0 rounded-none" v-if="getToolVisible(item) && item.dropdown">
<template #overlay>
<div class="min-w-[100px] bg-white rounded shadow-md p-4" :class="item.dropdownClass">
<draggable
v-if="item.dropdown === 'headset'"
v-model="dragColumns"
item-key="dataIndex"
@end="onDragEnd"
handle=".drag-handle"
>
<template v-for="element in dragColumns" :key="element.name">
<div @click.stop class="flex items-center mb-2 last:mb-0 select-none">
<span class="drag-handle mr-2 text-gray-400 hover:text-gray-600 cursor-move"></span>
<Checkbox
:checked="element.visible !== false"
@change.stop.prevent="
(e) => {
onColumnVisibleChanged(element, e.target.checked);
}
"
class="text-gray-700 hover:text-gray-900 select-none"
>
{{ element.title }}
</Checkbox>
</div>
</template>
</draggable>
</div>
</template>
<Button
:class="[
'px-[8px] py-[2px] relative border-[#ccc] bg-[#fcfcfc] rounded-none text-[#666] hover:z-10',
index === 0 ? 'rounded-l-[5px]' : '',
index === tools.length - 1 ? 'rounded-r-[5px]' : '',
]"
:disabled="getToolStatus(item)"
:tiptext="item.label"
@click="onToolClicked(item, pageData)"
>
<ToolIcon :icon="item.icon" class="w-[18px] h-[18.5px]" />
</Button>
</Dropdown>
<Button
v-else-if="!item.children && getToolVisible(item)"
:class="[
'px-[8px] py-[2px] relative border-[#ccc] bg-[#fcfcfc] rounded-none text-[#666] hover:z-10',
index === 0 ? 'rounded-l-[5px]' : '',
index === tools.length - 1 ? 'rounded-r-[5px]' : '',
]"
:disabled="getToolStatus(item)"
:tiptext="item.label"
@click="onToolClicked(item, pageData)"
>
<ToolIcon :icon="item.icon" class="w-[18px] h-[18.5px]" />
</Button>
<Dropdown placement="bottomRight" class="p-0 rounded-none" v-else-if="getToolVisible(item)">
<template #overlay>
<Menu>
<MenuItem v-for="menu in item.children" :key="menu.key" :disabled="getToolStatus(menu)">
{{ menu.label }}
</MenuItem>
</Menu>
</template>
<Button
:class="[
'!w-[46px] px-[5px] py-[2px] relative border-[#ccc] bg-[#fcfcfc] rounded-none text-[#666] hover:z-10',
index === 0 ? 'rounded-l-[5px]' : '',
]"
:disabled="getToolStatus(item)"
:tiptext="item.label"
:icon="item.icon"
:iconProps="{ class: 'w-[19px] h-[19px]' }"
@click="onToolClicked(item, pageData)"
>
<template #default>
<ToolIcon icon="icon-down-arrow" class="w-[12px] h-[12px]" />
</template>
</Button>
</Dropdown>
</template>
<!--
导出Excel 基于表头导出
导出PDF
TODO支持多模板导出选择
表头设置
TODO增加保存功能可设置个性化表头和多模式表头
-->
</div>
</template>

View File

@ -0,0 +1,72 @@
<script setup lang="ts" generic="T">
import { ButtonTool, PageData, getToolGroup, getToolStatus, onToolClicked, openNewForm } from '@skyfox2000/webbase';
import { Dropdown, Menu, MenuItem, Space } from 'ant-design-vue';
import { Button } from '@/components/common';
import { defineAsyncComponent } from 'vue';
//
const IconTool = defineAsyncComponent(() => import('./icontool.vue'));
const props = defineProps<{
/**
* 页面主数据
*/
pageData: PageData<T>;
}>();
const gridData = props.pageData.grid!;
const defaultButtons: ButtonTool[] = [
{
key: 'New',
label: '新增',
type: 'primary',
icon: 'icon-new',
danger: true,
click: openNewForm,
},
];
const MaxButtonCount = 3;
const { buttons, menus } = getToolGroup(
defaultButtons,
gridData.flat !== undefined ? gridData.flat : MaxButtonCount,
gridData.buttons,
);
</script>
<template>
<div class="flex justify-between mb-[10px]">
<Space>
<Button
v-for="item in buttons"
:key="item.key"
:type="item.type"
:danger="item.danger"
:disabled="getToolStatus(item)"
:icon="item.icon"
@click="onToolClicked(item, pageData)"
>
{{ item.label }}
</Button>
<Dropdown v-if="menus.length > 0">
<template #overlay>
<Menu>
<MenuItem
v-for="item in menus"
:key="item.key"
:disabled="getToolStatus(item)"
@click="onToolClicked(item, pageData)"
>
{{ item.label }}
</MenuItem>
</Menu>
</template>
<Button> 更多操作 </Button>
</Dropdown>
<!-- 空占位元素 -->
<span v-if="buttons.length === 0 && menus.length === 0"></span>
</Space>
<Space class="mr-1">
<component :is="IconTool" :page-data="pageData" />
</Space>
</div>
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import { Result, Button } from 'ant-design-vue';
import router from '@/router';
const onClicked = () => {
router.back();
};
</script>
<template>
<Result status="403" title="403" sub-title="您没有权限访问当前页面">
<template #extra>
<Button type="primary" @click="onClicked">点击返回</Button>
</template>
</Result>
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import { Result, Button } from 'ant-design-vue';
import router from '@/router';
const onClicked = () => {
router.back();
};
</script>
<template>
<Result status="404" title="404" sub-title="页面不存在或者没有权限访问页面">
<template #extra>
<Button type="primary" @click="onClicked">点击返回</Button>
</template>
</Result>
</template>

View File

@ -0,0 +1,108 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { AutoComplete, AutoCompleteOption } from 'ant-design-vue';
import {
circleLoading,
useInputFactory,
OptionCommProps,
OptionItemProps,
onOptionChanged,
loadOption,
unloadOption,
} from '@skyfox2000/webbase';
import { ReqParams } from '@skyfox2000/fapi';
const props = defineProps({
...OptionCommProps,
autoload: {
type: Boolean,
default: false,
},
onsearch: {
type: Function,
required: true,
},
});
const inputFactory = useInputFactory();
const { errClass, labelText } = inputFactory;
const emit = defineEmits(['change', 'update:label-info']);
inputFactory.inputEmit = emit;
/**
* 实际的选择项
*/
const selectOptions = ref<OptionItemProps[]>([]);
const onSearch = (value: string) => {
selectOptions.value = [];
let search_value = value.trim();
let query: ReqParams = { Query: {} };
if (props.onsearch) {
props.onsearch(search_value, query);
}
loadOption(true, props, selectOptions, inputFactory, query);
};
const onSelected = (value: any, _option: any) => {
onOptionChanged(props, value as number | string, selectOptions, inputFactory);
// const labelInfo: string[] = [];
// if (typeof selectedOptions === 'object') selectedOptions = [selectedOptions];
// selectedOptions.forEach((selectedOption: OptionItemProps) => {
// labelInfo.push(selectedOption.label);
// });
// emit('update:label-info', labelInfo);
};
onMounted(() => {
if (props.url && !props.url.fieldMap && !props.fieldMap) {
props.url.fieldMap = {
title: 'Name',
label: 'Name',
value: 'Id',
key: 'Id',
};
}
});
onUnmounted(() => {
unloadOption(props, inputFactory);
});
</script>
<template>
<div>
<div
v-if="props.url && props.url.loading === true"
class="absolute z-10 mt-[5px] mr-[10px] text-[#999] flex items-center"
>
<circleLoading class="text-[#555] mx-[5px] !ml-[10px] !w-4 !h-4" />
<span>数据加载中...</span>
</div>
<AutoComplete
:class="[errClass]"
@search="onSearch"
@select="onSelected"
:placeholder="props.url && !props.url.loading ? '请输入并选择' + labelText : ''"
v-bind="$attrs"
:label-in-value="false"
>
<template v-for="item in selectOptions" :key="item.value">
<AutoCompleteOption :value="item.value">
{{ item.label }}
</AutoCompleteOption>
</template>
</AutoComplete>
</div>
</template>
<style scoped>
.error :deep(.ant-select-selector) {
border-color: #ef4444;
box-shadow: 0 0 3px 0 #ff4d4f;
}
</style>

View File

@ -0,0 +1,76 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import {
circleLoading,
useInputFactory,
OptionCommProps,
OptionItemProps,
onOptionChanged,
loadOption,
unloadOption,
} from '@skyfox2000/webbase';
import { Cascader } from 'ant-design-vue';
const props = defineProps(OptionCommProps);
const inputFactory = useInputFactory();
const { errClass, labelText } = inputFactory;
const emit = defineEmits(['change', 'update:label-info']);
inputFactory.inputEmit = emit;
/**
* 实际的选择项
*/
const selectOptions = ref<OptionItemProps[]>([]);
const onChanged = (value: any, selectedOptions: any) => {
onOptionChanged(props, value as number | string, selectOptions, inputFactory);
const labelInfo: string[] = [];
if (selectedOptions) {
selectedOptions.forEach((option: OptionItemProps) => {
labelInfo.push(option.label);
});
}
emit('update:label-info', labelInfo);
};
onMounted(() => {
if (props.url && !props.url.fieldMap && !props.fieldMap) {
props.url.fieldMap = {
title: 'Name',
label: 'Name',
value: 'Id',
key: 'Id',
};
}
loadOption(props.autoload, props, selectOptions, inputFactory);
});
onUnmounted(() => {
unloadOption(props, inputFactory);
});
</script>
<template>
<div>
<div v-if="!selectOptions.length" class="absolute z-10 mt-[5px] mr-[10px] text-[#999] flex items-center">
<circleLoading class="text-[#555] mx-[5px] !ml-[10px] !w-4 !h-4" />
<span>数据加载中...</span>
</div>
<Cascader
:options="selectOptions"
:class="[errClass]"
:allow-clear="true"
:placeholder="selectOptions.length > 0 ? '请选择' + labelText : ''"
@change="onChanged"
v-bind="$attrs"
/>
</div>
</template>
<style scoped>
.error :deep(.ant-select-selector) {
border-color: #ef444480;
box-shadow: 0 0 3px 0 #ff4d4f;
}
</style>

View File

@ -0,0 +1,40 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { Checkbox, CheckboxGroup } from 'ant-design-vue';
import { loadOption, onOptionChanged, OptionItemProps, unloadOption, useInputFactory } from "@skyfox2000/webbase";
import { CheckboxValueType } from "ant-design-vue/es/checkbox/interface";
import { OptionCommProps } from "@skyfox2000/webbase";
const props = defineProps(OptionCommProps);
const inputFactory = useInputFactory();
const checkboxOptions = ref<OptionItemProps[]>([]);
const onChanged = (e: CheckboxValueType[]) => {
const checkedValue = e;
onOptionChanged(props, checkedValue as (number | string)[], checkboxOptions, inputFactory);
}
onMounted(() => {
if (props.url && !props.url.fieldMap && !props.fieldMap) {
props.url.fieldMap = {
title: 'Name',
label: 'Name',
value: 'Id',
key: 'Id',
};
}
loadOption(props.autoload, props, checkboxOptions, inputFactory)
});
onUnmounted(() => {
unloadOption(props, inputFactory);
});
</script>
<template>
<CheckboxGroup @change="onChanged" class="w-full" v-bind="$attrs">
<Checkbox v-for="item in checkboxOptions" :key="item.value" :value="item.value"">
{{ item.label }}
</Checkbox>
</CheckboxGroup>
</template>

View File

@ -0,0 +1,27 @@
<script setup lang="ts">
import { ref } from 'vue';
import { DatePicker } from 'ant-design-vue';
import locale from 'ant-design-vue/es/date-picker/locale/zh_CN';
import { useInputFactory } from '@skyfox2000/webbase';
const props = defineProps<{
valueFormat?: string;
}>();
const inputFactory = useInputFactory();
const { labelText, errClass } = inputFactory;
const dateFormat = ref<string>(props.valueFormat ?? 'YYYY-MM-DD');
</script>
<template>
<DatePicker
:class="
errClass === 'error'
? ['error', '!border-red-300', 'shadow-[0_0_3px_0px_#ff4d4f]']
: ''
"
class="w-full"
:placeholder="'请选择' + labelText"
:locale="locale"
:valueFormat="dateFormat"
/>
</template>

View File

@ -0,0 +1,56 @@
<script setup lang="ts">
import { formValidate, useInputFactory } from '@skyfox2000/webbase';
import { Input } from 'ant-design-vue';
import { ref, watch } from 'vue';
const { editorData, labelText, errClass } = useInputFactory();
const onBlur = () => {
if (errClass.value && editorData.value) {
///
formValidate(editorData.value);
}
};
const props = defineProps<{
value?: any;
}>();
const emit = defineEmits(['update:value']);
const defaultValue = props.value;
// value
const innerValue = ref(props.value);
// value
watch(
() => props.value,
(newValue) => {
innerValue.value = newValue;
},
{ immediate: true }, //
);
watch(
() => innerValue.value,
(newValue) => {
emit('update:value', newValue); // value
},
{ deep: true },
);
const onClear = () => {
if (innerValue.value === '') {
emit('update:value', defaultValue);
return;
}
};
</script>
<template>
<Input
:class="errClass === 'error' ? ['error', '!border-red-300', 'shadow-[0_0_3px_0px_#ff4d4f]'] : ''"
v-model:value="innerValue"
@change="onClear"
:allow-clear="true"
:placeholder="'请输入' + labelText"
@blur="onBlur"
v-bind="$attrs"
/>
</template>

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
import { formValidate, useInputFactory } from '@skyfox2000/webbase';
import { InputNumber } from 'ant-design-vue';
const { editorData, labelText, errClass } = useInputFactory();
const onBlur = () => {
if (errClass.value && editorData.value) {
///
formValidate(editorData.value);
}
};
</script>
<template>
<InputNumber
:class="errClass === 'error' ? ['error', '!border-red-300', 'shadow-[0_0_3px_0px_#ff4d4f]'] : ''"
@blur="onBlur"
:allow-clear="false"
:placeholder="'请输入' + labelText"
class="w-[50%]"
v-bind="$attrs"
/>
</template>

View File

@ -0,0 +1,16 @@
<script setup lang="ts">
import { formValidate, useInputFactory } from "@skyfox2000/webbase";
import { InputPassword } from "ant-design-vue";
const { editorData, labelText, errClass } = useInputFactory();
const onBlur = () => {
if (errClass.value && editorData.value) {
///
formValidate(editorData.value)
}
}
</script>
<template>
<InputPassword :class="errClass === 'error' ? ['error', '!border-red-300', 'shadow-[0_0_3px_0px_#ff4d4f]'] : ''"
:allow-clear="true" :placeholder="'请输入' + labelText" @blur="onBlur" v-bind="$attrs" />
</template>

View File

@ -0,0 +1,72 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { Input, Button } from 'ant-design-vue';
interface ConfigItem {
field: string;
value: string;
}
const props = defineProps<{
value: Record<string, string>;
labelHolder?: string;
valueHolder: string;
}>();
const emit = defineEmits(['update:config']);
const configList = ref<ConfigItem[]>([]);
const initConfigList = () => {
configList.value = Object.entries(props.value).map(([field, value]) => ({
field,
value,
}));
};
watch(() => props.value, () => {
initConfigList();
}, { immediate: true });
const updateConfig = () => {
const newConfig = configList.value.reduce((acc, item) => {
if (item.field) {
acc[item.field] = item.value;
}
return acc;
}, {} as Record<string, string>);
emit('update:config', newConfig);
};
const addNewLine = () => {
configList.value.push({
field: '',
value: '',
});
};
const handleInputChange = () => {
updateConfig();
};
</script>
<template>
<div class="flex flex-col gap-2">
<div v-for="item in configList" :key="item.field" class="flex items-center gap-2">
<div class="w-[33%]">
<Input v-model:value="item.field" class="w-full" :placeholder="labelHolder || '配置名'"
@change="handleInputChange" />
</div>
<div class="w-[3%]">
=
</div>
<div class="w-[64%]">
<Input v-model:value="item.value" :placeholder="valueHolder" @change="handleInputChange" />
</div>
</div>
<Button @click="addNewLine"
class="mt-1 w-[80px] !text-[12px] text-[#666] bg-[#e6f7ff] border-[#b3e0ff] hover:bg-[#b3e0ff] hover:border-[#8abeff]"
size="small">
新增配置行
</Button>
</div>
</template>

View File

@ -0,0 +1,48 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { Radio, RadioChangeEvent, RadioGroup } from 'ant-design-vue';
import { loadOption, onOptionChanged, OptionItemProps, unloadOption, useInputFactory } from '@skyfox2000/webbase';
import { OptionCommProps } from '@skyfox2000/webbase';
const props = defineProps(OptionCommProps);
const inputFactory = useInputFactory();
const { errClass } = inputFactory;
const radioOptions = ref<OptionItemProps[]>([]);
const onChanged = (e: RadioChangeEvent) => {
const checkedValue = e.target.value;
onOptionChanged(props, checkedValue as number | string, radioOptions, inputFactory);
};
onMounted(() => {
if (props.url && !props.url.fieldMap && !props.fieldMap) {
props.url.fieldMap = {
title: 'Name',
label: 'Name',
value: 'Id',
key: 'Id',
};
}
loadOption(props.autoload, props, radioOptions, inputFactory);
if (props.all) radioOptions.value.unshift({ label: '全部', value: undefined });
});
onUnmounted(() => {
unloadOption(props, inputFactory);
});
</script>
<template>
<RadioGroup :autocheck="false" @change="onChanged" class="w-full flex align-items" v-bind="$attrs">
<template v-for="item in radioOptions" :key="item.value">
<Radio :value="item.value" :class="errClass === 'error' ? ['error', '!text-red-400'] : ''">
{{ item.label }}
</Radio>
</template>
</RadioGroup>
</template>
<style scoped>
.error :deep(input + span) {
border-color: #ff7171;
box-shadow: 0 0 3px 0 #ff4d4f;
}
</style>

View File

@ -0,0 +1,73 @@
<script setup lang="ts">
import { computed } from 'vue';
import { RangePicker } from 'ant-design-vue';
import locale from 'ant-design-vue/es/date-picker/locale/zh_CN';
import { useInputFactory } from '@skyfox2000/webbase';
import dayjs, { Dayjs } from 'dayjs';
const props = withDefaults(
defineProps<{
startDate?: string | null;
endDate?: string | null;
valueFormat?: string;
}>(),
{
valueFormat: 'YYYY-MM-DD',
},
);
const emit = defineEmits<{
(e: 'update:startDate', value: string | null): void;
(e: 'update:endDate', value: string | null): void;
}>();
const inputFactory = useInputFactory();
const { errClass } = inputFactory;
const dateFormat = computed(() => props.valueFormat);
const rangeValue = computed(
(): [Dayjs, Dayjs] | [string, string] | undefined => {
const start = props.startDate;
const end = props.endDate;
if (!start || !end) return undefined;
try {
const startDayjs = dayjs(start);
const endDayjs = dayjs(end);
if (!startDayjs.isValid() || !endDayjs.isValid()) return undefined;
return [startDayjs, endDayjs];
} catch {
return undefined;
}
},
);
const handleChange = (
dates: [Dayjs, Dayjs] | [string, string] | null,
dateStrings: [string, string],
) => {
if (!dates || !dateStrings || dateStrings.length !== 2) {
emit('update:startDate', null);
emit('update:endDate', null);
return;
}
emit('update:startDate', dateStrings[0] || null);
emit('update:endDate', dateStrings[1] || null);
};
</script>
<template>
<RangePicker
:class="
errClass === 'error'
? ['error', '!border-red-300', 'shadow-[0_0_3px_0px_#ff4d4f]']
: ''
"
class="w-full"
:locale="locale"
:value-format="dateFormat"
:value="rangeValue"
@change="handleChange"
/>
</template>

View File

@ -0,0 +1,86 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { Select, SelectOption } from 'ant-design-vue';
import type { DefaultOptionType, SelectValue } from 'ant-design-vue/es/select';
import {
circleLoading,
useInputFactory,
OptionCommProps,
OptionItemProps,
onOptionChanged,
loadOption,
unloadOption,
} from '@skyfox2000/webbase';
const props = defineProps(OptionCommProps);
const inputFactory = useInputFactory();
const { errClass, labelText } = inputFactory;
const emit = defineEmits(['change', 'update:label-info']);
inputFactory.inputEmit = emit;
/**
* 实际的选择项
*/
const selectOptions = ref<OptionItemProps[]>([]);
const onChanged = (value: SelectValue, selectedOptions: DefaultOptionType | DefaultOptionType[]) => {
onOptionChanged(props, value as number | string, selectOptions, inputFactory);
const labelInfo: string[] = [];
if (typeof selectedOptions === 'object') selectedOptions = [selectedOptions];
selectedOptions.forEach((selectedOption: OptionItemProps) => {
labelInfo.push(selectedOption.label);
});
emit('update:label-info', labelInfo);
};
onMounted(() => {
if (props.url && !props.url.fieldMap && !props.fieldMap) {
props.url.fieldMap = {
title: 'Name',
label: 'Name',
value: 'Id',
key: 'Id',
};
}
loadOption(props.autoload, props, selectOptions, inputFactory);
});
onUnmounted(() => {
unloadOption(props, inputFactory);
});
</script>
<template>
<div>
<div
v-if="props.url && props.url.loading === true"
class="absolute z-10 mt-[5px] mr-[10px] text-[#999] flex items-center"
>
<circleLoading class="text-[#555] mx-[5px] !ml-[10px] !w-4 !h-4" />
<span>数据加载中...</span>
</div>
<Select
:class="[errClass]"
@change="onChanged"
:placeholder="props.url && !props.url.loading ? '请选择' + labelText : ''"
v-bind="$attrs"
:label-in-value="false"
>
<template v-for="item in selectOptions" :key="item.value">
<SelectOption :value="item.value">
{{ item.label }}
</SelectOption>
</template>
</Select>
</div>
</template>
<style scoped>
.error :deep(.ant-select-selector) {
border-color: #ef4444;
box-shadow: 0 0 3px 0 #ff4d4f;
}
</style>

View File

@ -0,0 +1,59 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import type { PropType } from 'vue';
import { formValidate, useInputFactory, OptionItemProps, loadOption, unloadOption } from '@skyfox2000/webbase';
import { Switch } from 'ant-design-vue';
import message from 'vue-m-message';
const props = defineProps({
/**
* 选择项数据
*/
data: {
type: Array as PropType<Record<string, any>[]>,
required: true,
},
});
/**
* 实际的选择项
*/
const switchOptions = ref<OptionItemProps[]>([]);
const emit = defineEmits<{
(e: 'change', checked: boolean | string | number): void;
}>();
const { editorData, errClass } = useInputFactory();
const onChange = (checked: boolean | string | number) => {
if (errClass.value && editorData.value) {
///
formValidate(editorData.value);
}
emit('change', checked);
};
onMounted(() => {
if (!props.data || props.data.length != 2) {
console.error('Switch组件: ', props.data);
message.error('Switch组件必须有且只有两个选项');
return;
}
loadOption(false, props, switchOptions);
});
onUnmounted(() => {
unloadOption(props);
});
</script>
<template>
<Switch
v-if="switchOptions.length === 2"
:class="[errClass === 'error' ? 'error !border-red-300 shadow-[0_0_3px_0px_#ff4d4f]' : '', 'w-[58px]']"
:checkedChildren="switchOptions[0].label"
:checkedValue="switchOptions[0].value"
:unCheckedChildren="switchOptions[1].label"
:unCheckedValue="switchOptions[1].value"
@change="onChange"
v-bind="$attrs"
/>
</template>

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import { formValidate, useInputFactory } from '@skyfox2000/webbase';
import { Textarea } from 'ant-design-vue';
const { editorData, labelText, errClass } = useInputFactory();
const onBlur = () => {
if (errClass.value && editorData.value) {
///
formValidate(editorData.value);
}
};
</script>
<template>
<Textarea
:class="errClass === 'error' ? ['error', '!border-red-300', 'shadow-[0_0_3px_0px_#ff4d4f]'] : ''"
:allow-clear="true"
:placeholder="'请输入' + labelText"
@blur="onBlur"
v-bind="$attrs"
/>
</template>

43
src/components/index.ts Normal file
View File

@ -0,0 +1,43 @@
export { default as Button } from './common/button/index.vue';
export { default as Appicon } from './common/icon/appicon.vue';
export { default as Fullscreen } from './common/icon/fullscreen.vue';
export { default as Helper } from './common/icon/helper.vue';
export { default as Icon } from './common/icon/index.vue';
export { default as LayoutIcon } from './common/icon/layoutIcon.vue';
export { default as ToolIcon } from './common/icon/toolIcon.vue';
export { default as Tooltip } from './common/tooltip/index.vue';
export { default as Dialog } from './content/dialog/index.vue';
export { default as UploadForm } from './content/dialog/uploadForm.vue';
export { default as UploadList } from './content/dialog/uploadList.vue';
export { default as Drawer } from './content/drawer/index.vue';
export { default as FormItem } from './content/form/formItem.vue';
export { default as Form } from './content/form/index.vue';
export { default as Search } from './content/search/index.vue';
export { default as SearchItem } from './content/search/searchItem.vue';
export { default as Table } from './content/table/index.vue';
export { default as TableOperate } from './content/table/tableOperate.vue';
export { default as Icontool } from './content/toolbar/icontool.vue';
export { default as Toolbar } from './content/toolbar/index.vue';
export { default as Error403 } from './error/error403.vue';
export { default as Error404 } from './error/error404.vue';
export { default as AutoComplete } from './form/autoComplete/index.vue';
export { default as Cascader } from './form/cascader/index.vue';
export { default as Checkbox } from './form/checkbox/index.vue';
export { default as DatePicker } from './form/datePicker/index.vue';
export { default as Input } from './form/input/index.vue';
export { default as InputNumber } from './form/input/inputNumber.vue';
export { default as InputPassword } from './form/input/inputPassword.vue';
export { default as PropEditor } from './form/propEditor/index.vue';
export { default as Radio } from './form/radio/index.vue';
export { default as RangePicker } from './form/rangePicker/index.vue';
export { default as Select } from './form/select/index.vue';
export { default as Switch } from './form/switch/index.vue';
export { default as Textarea } from './form/textarea/index.vue';
export { default as Breadcrumb } from './layout/breadcrumb/index.vue';
export { default as Content } from './layout/content/index.vue';
export { default as Datetime } from './layout/datetime/index.vue';
export { default as HeaderExits } from './layout/header/headerExits.vue';
export { default as Header } from './layout/header/index.vue';
export { default as Menu } from './layout/menu/index.vue';
export { default as MenuTabs } from './layout/menu/menuTabs.vue';
export { default as BasicLayout } from './layout/page/basicLayout.vue';

View File

@ -0,0 +1,38 @@
<script setup lang="ts">
import { onMounted, watch } from 'vue';
import { Breadcrumb, theme } from 'ant-design-vue';
import { LayoutIcon } from '@/components';
import { BreadcrumbRoute, showBreadcrumb, crumbs, usePageInfo } from '@skyfox2000/webbase';
const { useToken } = theme;
const { token } = useToken();
const pageInfoStore = usePageInfo();
watch(
() => pageInfoStore.TabActive,
() => showBreadcrumb(),
);
onMounted(() => {
showBreadcrumb();
});
</script>
<template>
<div
class="ml-5 h-fit p-0 flex items-center justify-between"
:style="{
backgroundColor: token.colorBgContainer,
}"
>
<LayoutIcon icon="icon-home" class="w-[15px]" />
<span class="leading-[2.5] mx-[6px] text-[rgba(0,0,0,0.45)]">&gt;</span>
<Breadcrumb :routes="crumbs" separator="">
<template #itemRender="{ route }: { route: BreadcrumbRoute }">
<span class="text-xs leading-[3]">{{ route.breadcrumbName }}</span>
<LayoutIcon :icon="route.icon" fontsize="15px" />
<span v-if="route.index! < crumbs.length - 1" class="leading-[2.5] mx-[6px] text-[rgba(0,0,0,0.45)]"
>&gt;</span
>
</template>
</Breadcrumb>
</div>
</template>

View File

@ -0,0 +1,15 @@
<script lang="ts" setup>
import { LayoutContent, theme } from 'ant-design-vue';
const { useToken } = theme;
const { token } = useToken();
</script>
<template>
<div class="relative h-[calc(100vh-80px)] overflow-y-auto">
<LayoutContent class="m-[10px] p-[10px] h-fit min-h-[calc(100vh-100px)]" :style="{
backgroundColor: token.colorBgContainer,
}">
<slot></slot>
</LayoutContent>
</div>
</template>

View File

@ -0,0 +1,16 @@
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
const DateTime = ref("")
onMounted(() => {
setInterval(() => {
const curTime = new Date()
const options: any = { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' };
const formattedTime = curTime.toLocaleString(undefined, options).replace(/\//g, '-').replace(',', '');
DateTime.value = curTime.getFullYear() + "-" + formattedTime
}, 1000);
})
</script>
<template>
<div class="font-['Courier'] text-[#666]">{{ DateTime }}</div>
</template>

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Modal, Flex } from 'ant-design-vue';
import { useUserInfo } from '@skyfox2000/webbase';
import { LayoutIcon } from '@/components';
const userInfoStore = useUserInfo();
const open = ref(false);
const confirmExit = () => {
userInfoStore.logout();
};
</script>
<template>
<LayoutIcon icon="icon-logout" @click="open = true" class="cursor-pointer w-5 h-5" />
<Modal v-model:open="open" title="确定退出?" ok-text="确定" cancel-text="取消" :width="380" @ok="confirmExit">
<Flex align="center" justify="flex-start" :style="{ padding: '0 32px', margin: '20px 0' }">
<LayoutIcon icon="icon-question-circle" color="orange" class="w-[60px] h-[60px]" />
<div style="margin: 0 0 0 20px; font-weight: 400; font-size: 16px">是否退出系统<br />清除用户缓存信息</div>
</Flex>
</Modal>
</template>

View File

@ -0,0 +1,43 @@
<script lang="ts" setup>
import { LayoutHeader, Avatar, theme, Space } from 'ant-design-vue';
import { useSettingInfo } from '@skyfox2000/webbase';
import { LayoutIcon } from '@/components';
import Breadcrumb from '../breadcrumb/index.vue';
import HeaderExits from './headerExits.vue';
const { useToken } = theme;
const { token } = useToken();
const settingInfoStore = useSettingInfo();
const onCollapseClick = () => {
settingInfoStore.setMenuCollapse(!settingInfoStore.menuCollapse);
};
</script>
<template>
<LayoutHeader
class="w-full relative z-[1] shadow-[0_-3px_6px_#000] py-0 flex items-center justify-between"
:style="{
height: '40px',
lineHeight: '1',
paddingLeft: '10px',
paddingRight: '10px',
backgroundColor: token.colorBgContainer,
}"
>
<div class="flex items-center">
<LayoutIcon
icon="icon-menu"
class="w-[18px] h-[18px] cursor-pointer"
:angle="settingInfoStore.menuCollapse ? 90 : 0"
@click="onCollapseClick"
/>
<Breadcrumb></Breadcrumb>
</div>
<div>
<Space size="middle" class="flex items-center">
<Avatar class="avatar" :style="{ backgroundColor: '#f56a00', fontSize: '14px' }" :size="24"> U </Avatar>
<HeaderExits />
</Space>
</div>
</LayoutHeader>
</template>

View File

@ -0,0 +1,69 @@
<script lang="ts" setup>
import { nextTick, onMounted, reactive, ref, watch } from 'vue';
import { ItemType, Menu } from 'ant-design-vue';
import type { MenuProps } from 'ant-design-vue';
import { routes } from '@/router';
import { AppRouter, initMenu, useSettingInfo, useAppInfo, usePageInfo } from '@skyfox2000/webbase';
import { LayoutIcon } from '@/components';
const openKeys = ref<string[]>([]);
const selectedKeys = ref<string[]>([]);
//
const menuData: Array<ItemType> = reactive([]);
const pageInfoStore = usePageInfo();
const onMenuClick: MenuProps['onClick'] = (menuInfo) => {
useAppInfo().push(menuInfo.key.toString());
};
const settingInfoStore = useSettingInfo();
const showOpenKeys = ref<string[]>([]);
const activeMenu = () => {
let subPath = pageInfoStore.TabActive;
const paths = subPath.split('/');
paths.pop();
openKeys.value = [paths.join('/')];
if (!settingInfoStore.menuCollapse) {
showOpenKeys.value = [paths.join('/')];
}
selectedKeys.value = [subPath];
};
watch(
() => settingInfoStore.menuCollapse,
(newVal) => {
if (!newVal) {
showOpenKeys.value = [];
nextTick(() => {
showOpenKeys.value = [...openKeys.value];
});
}
},
);
watch(
() => pageInfoStore.TabActive,
() => {
activeMenu();
},
);
onMounted(() => {
initMenu<ItemType>(routes, menuData, LayoutIcon, { class: '!w-5 !h-5' });
pageInfoStore.setTabActive(AppRouter.currentRoute.value.path);
activeMenu();
});
</script>
<template>
<Menu
v-model:openKeys="showOpenKeys"
v-model:selectedKeys="selectedKeys"
mode="inline"
theme="dark"
:items="menuData"
@click="onMenuClick"
>
</Menu>
</template>

View File

@ -0,0 +1,55 @@
<script setup lang="ts">
import { Tabs, TabPane, theme } from 'ant-design-vue';
import { useAppInfo, usePageInfo } from '@skyfox2000/webbase';
import { LayoutIcon, Tooltip } from '@/components';
const { useToken } = theme;
const { token } = useToken();
const pageInfoStore = usePageInfo();
const onTabClicked = (tabKey: any) => {
useAppInfo().push(tabKey as string);
};
const closeTab = (key: string) => {
pageInfoStore.removeTabPane(key);
useAppInfo().push(pageInfoStore.TabActive);
};
</script>
<template>
<div :style="{ height: '38px', backgroundColor: token.colorBgBase }">
<Tabs
:activeKey="pageInfoStore.TabActive"
hide-add
size="small"
:tabBarStyle="{ padding: '0 20px' }"
@tabClick="onTabClicked"
>
<TabPane v-for="pane in pageInfoStore.TabPanes" :key="pane.key">
<template #tab>
<div class="flex items-center">
<span class="flex">
{{ pane.title }}
</span>
<Tooltip title="关闭" placement="top" size="small">
<div
class="inline-block mx-auto relative flex items-center"
v-if="pageInfoStore.TabPanes.length > 1 && pane.closable"
@click.stop="closeTab(pane.key)"
>
<LayoutIcon
icon="icon-add"
:angle="45"
class="w-[15px] h-[15px] ml-1"
:tipcolor="token.colorBgSpotlight"
/>
</div>
</Tooltip>
</div>
</template>
</TabPane>
</Tabs>
</div>
</template>

View File

@ -0,0 +1,54 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { Layout, LayoutSider } from 'ant-design-vue';
import Tooltip from '../../common/tooltip/index.vue';
import Icon from '../../common/icon/index.vue';
import Menu from '../menu/index.vue';
import Header from '../header/index.vue';
import MenuTabs from '../menu/menuTabs.vue';
import { useAppInfo, usePageInfo, useSettingInfo } from '@skyfox2000/webbase';
const appInfoStore = useAppInfo();
const settingInfoStore = useSettingInfo();
const pageInfoStore = usePageInfo();
const bodyClass = ref('h-[calc(100vh-80px)]');
watch(
() => settingInfoStore.fullscreen,
(newVal) => {
bodyClass.value = newVal ? 'h-[calc(100vh-40px)]' : 'h-[calc(100vh-80px)]';
},
);
</script>
<template>
<Layout class="h-screen">
<LayoutSider
class="overflow-auto h-screen left-0 top-0 bottom-0"
v-model:collapsed="settingInfoStore.menuCollapse"
collapsible
v-if="!settingInfoStore.fullscreen"
>
<div
class="h-[40px] max-h-[40px] bg-[rgba(240,240,240,0.2)] flex flex-nowrap items-center justify-center text-white font-bold text-lg overflow-hidden text-ellipsis"
>
<Tooltip :title="settingInfoStore.menuCollapse ? appInfoStore.appInfo.Name : ''" placement="right">
<Icon :icon="appInfoStore.appInfo.Icon" fontsize="30px" size="26px" />
</Tooltip>
<span v-if="!settingInfoStore.menuCollapse" class="ml-[10px]">{{ appInfoStore.appInfo.Name }}</span>
</div>
<Menu></Menu>
</LayoutSider>
<Layout class="overflow-y-auto block">
<Header v-if="!settingInfoStore.fullscreen"></Header>
<MenuTabs v-if="pageInfoStore.TabEnabled"></MenuTabs>
<div class="relative overflow-y-auto" :class="bodyClass">
<router-view v-slot="{ Component, route }">
<keep-alive :include="appInfoStore.CachedComponents" :exclude="appInfoStore.ExcludeComponents">
<component :is="appInfoStore.cacheComponent(Component, route)" />
</keep-alive>
</router-view>
</div>
</Layout>
</Layout>
</template>

67
src/main.ts Normal file
View File

@ -0,0 +1,67 @@
import '@/assets/styles/global.css';
import 'vue-m-message/dist/style.css';
import Message from 'vue-m-message';
import { createApp } from 'vue';
//引入状态管理
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import { initMainAppData, isMicroApp } from '@skyfox2000/microbase';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
import App from '@/App.vue';
//引入路由
import router from '@/router';
import { appList, appRoutes } from '@/router/app-routes';
import { initValidate, useUserInfo, useHostInfo, useAppInfo } from '@skyfox2000/webbase';
import { SERVER_HOST } from '@skyfox2000/fapi';
const initializeApp = async () => {
initValidate();
const app = createApp(App);
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
app.use(pinia);
app.use(Message);
const userInfoStore = useUserInfo();
userInfoStore.init();
if (!isMicroApp()) {
// 本地应用,使用本地配置加载方式
const hostInfoStore = useHostInfo();
await hostInfoStore.loadHostInfo(import.meta.env.VITE_SITEHOST_API, import.meta.env.VITE_SITEHOST);
} else {
SERVER_HOST.TOOL_ICONS = import.meta.env.VITE_TOOL_ICONS;
SERVER_HOST.MICROLAYOUT_ICONS = import.meta.env.VITE_MICROLAYOUT_ICONS;
}
const appInfoStore = useAppInfo();
await appInfoStore.loadAppList(appList);
await appInfoStore.setAppRoutes(appRoutes);
app.use(router);
app.mount('#app');
if (import.meta.env.VITE_LOGIN_TEST == 'true') {
await userInfoStore.login({
UserName: import.meta.env.VITE_LOGIN_TEST_USERNAME,
UserPass: import.meta.env.VITE_LOGIN_TEST_PASSWORD,
});
} else {
}
};
if (isMicroApp()) {
initMainAppData();
initializeApp();
} else {
initializeApp();
}

78
src/router/app-routes.ts Normal file
View File

@ -0,0 +1,78 @@
import { AppAction, AppInfo, AppSource } from '@skyfox2000/microbase';
import '/public/layout/appicons.js';
import { parseIcons } from '@skyfox2000/webbase';
parseIcons({
iconUrl: '',
monoColor: false,
});
const BasicLayout = () => import('@/components/layout/page/basicLayout.vue');
export const Application: AppInfo = {
Id: 'hospital',
Name: '后台管理',
AppCode: 'hospital',
Version: '1.0',
Host: '/',
Source: AppSource.Manual,
Action: AppAction.App,
Default: true,
Icon: 'sym-app-setting',
Enabled: 1,
};
export const appList = [Application];
export const appRoutes = [
{
path: '/business',
name: '业务管理',
component: BasicLayout,
icon: 'icon-home',
children: [
{
path: 'order',
name: '订单管理',
component: () => import('@/views/business/order/index.vue'),
},
{
path: 'priceqr',
name: '价码管理',
component: () => import('@/views/business/priceqr/index.vue'),
},
{
path: 'helper',
name: '取样帮手',
component: () => import('@/views/business/helper/index.vue'),
},
],
},
{
path: '/system',
name: '系统管理',
component: BasicLayout,
icon: 'icon-setting',
children: [
{
path: 'member',
name: '会员管理',
component: () => import('@/views/system/member/index.vue'),
},
{
path: 'hospital',
name: '医院管理',
component: () => import('@/views/system/hospital/index.vue'),
},
{
path: 'doctor',
name: '医生管理',
component: () => import('@/views/system/doctor/index.vue'),
},
{
path: 'account',
name: '用户管理',
component: () => import('@/views/system/account/index.vue'),
},
],
},
];

60
src/router/index.ts Normal file
View File

@ -0,0 +1,60 @@
import { AppRouter, routes, useUserInfo } from '@skyfox2000/webbase';
const BasicLayout = () => import('@/components/layout/page/basicLayout.vue');
const NotFound404 = () => import('@/components/error/error404.vue');
/**
*
*/
const defaultRoutes = [
{
path: '/',
name: 'home',
redirect: '/business/order',
beforeEnter: () => {
const userInfoStore = useUserInfo();
if (!userInfoStore.isLogin) {
AppRouter.replace('/login');
return false;
}
return true;
},
},
{
path: '/login',
name: 'login',
component: () => import('@/views/login/index.vue'),
beforeEnter: () => {
const userInfoStore = useUserInfo();
if (userInfoStore.isLogin) {
AppRouter.replace('/business/order');
return false;
}
return true;
},
},
// 404 路由
{
path: '/error', // 使用通配符 :catchAll 匹配所有未匹配到的路由
name: 'NotFound',
component: BasicLayout,
children: [
{
path: '404',
name: '404',
component: NotFound404,
meta: { keepAlive: true },
},
],
},
];
defaultRoutes.forEach((route) => {
routes.push(route);
AppRouter.addRoute(route);
});
export { routes };
export default AppRouter;

343
src/types/global.d.ts vendored Normal file
View File

@ -0,0 +1,343 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {};
declare global {
/**
*
*/
interface SamplingHelperEntity extends import('src/views/business/helper/model')['SamplingHelperEntity'] {
/**
* ID
*/
Id: string | null;
/**
*
*/
DoctorId: string | null;
/**
*
*/
LocationInfo?: { [key: string]: any };
/**
*
*/
Process: string | null;
/**
*
*/
Enabled: number;
/**
*
*/
CreateTime?: string | null;
/**
*
*/
UpdateTime?: string | null;
}
/**
*
*/
interface OrderEntity extends import('src/views/business/order/model')['OrderEntity'] {
/**
* ID
*/
Id: string | null;
/**
*
*/
MemberId: string | null;
/**
*
*/
MemberName: string | null;
/**
* ID
*/
PriceId: string | null;
/**
*
*/
PriceCode: string | null;
/**
*
*/
ProductName: string | null;
/**
*
*/
Address: string | null;
/**
*
*/
ExpressNumber?: string | null;
/**
*
*/
ExpressInfo?: { [key: string]: any };
/**
*
*/
ReportFileUrl?: string | null;
/**
*
*/
Status: string | null;
/**
*
*/
Remark?: string | null;
/**
*
*/
Enabled: number;
/**
*
*/
CreateTime?: string | null;
/**
*
*/
UpdateTime?: string | null;
}
/**
*
*/
interface PriceQREntity extends import('src/views/business/priceqr/model')['PriceQREntity'] {
/**
* ID
*/
Id: string | null;
/**
*
*/
Code: string | null;
/**
*
*/
Name: string | null;
/**
*
*/
Price: number | null;
/**
*
*/
Remark?: string | null;
/**
*
*/
Enabled: number;
/**
*
*/
CreateTime?: string | null;
/**
*
*/
UpdateTime?: string | null;
}
/**
*
*/
interface AccountEntity extends import('src/views/system/account/model')['AccountEntity'] {
/**
* Admin管理员/User操作员
*/
AccountType: string | null;
/**
*
*/
ClientId?: string | null;
/**
*
*/
DepartId?: string | null;
/**
*
*/
Enabled: number;
/**
* Id
*/
Id: string | null;
/**
*
*/
JobTitleId?: string | null;
/**
*
*/
Name: string | null;
/**
*
*/
PassTime?: string | null;
/**
*
*/
Remark?: string | null;
/**
*
*/
State?: string | null;
/**
*
*/
Tags?: string[];
/**
*
*/
UpdateTime?: string | null;
/**
*
*/
UpdateUser?: string | null;
/**
*
*/
UserCode: string | null;
/**
* (Platform平台/Client租户/Member会员)
*/
UserLevel: string | null;
/**
*
*/
UserProps?: { [key: string]: any };
/**
*
*/
UserPwd?: string | null;
}
/**
*
*/
interface DoctorEntity extends import('src/views/system/doctor/model')['DoctorEntity'] {
/**
* ID
*/
Id: string | null;
/**
*
*/
Mobile: string | null;
/**
*
*/
Name: string | null;
/**
*
*/
HospitalId: string | null;
/**
*
*/
Enabled: number;
/**
*
*/
CreateTime?: string | null;
/**
*
*/
UpdateTime?: string | null;
}
/**
*
*/
interface HospitalEntity extends import('src/views/system/hospital/model')['HospitalEntity'] {
/**
* ID
*/
Id: string | null;
/**
*
*/
Name: string | null;
/**
*
*/
Address?: string | null;
/**
*
*/
Contact?: string | null;
/**
*
*/
LocationInfo?: { [key: string]: any };
/**
*
*/
Enabled: number;
/**
*
*/
CreateTime?: string | null;
/**
*
*/
UpdateTime?: string | null;
}
/**
*
*/
interface MemberEntity extends import('src/views/system/member/model')['MemberEntity'] {
/**
* ID
*/
Id: string | null;
/**
*
*/
Mobile: string | null;
/**
*
*/
Name: string | null;
/**
*
*/
Gender: '男' | '女' | null;
/**
*
*/
BirthDate: string | null;
/**
* openid
*/
WeChatOpenId: string | null;
/**
*
*/
Remark?: string | null;
/**
*
*/
Enabled: number;
/**
*
*/
CreateTime?: string | null;
/**
*
*/
UpdateTime?: string | null;
}
}

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Drawer, Form, FormItem, Textarea, Switch, Cascader } from '@/components';
import { pageData, editorData } from './page';
import { OPTIONS } from '@skyfox2000/webbase';
import { locationTreeUrl } from '../../system/location/page';
import DoctorSelect from '@/views/system/doctor/select.vue';
const formData = ref<SamplingHelperEntity>(editorData.formData);
</script>
<template>
<Drawer title="取样帮手配置" :page-data="pageData">
<Form>
<FormItem label="所在地区" rule="LocationInfo.LocationIds">
<Cascader
v-model:value="formData.LocationInfo!.LocationIds"
:url="locationTreeUrl"
v-model:label-info="formData.LocationInfo!.LocationNames"
/>
</FormItem>
<FormItem label="医生" rule="DoctorId">
<DoctorSelect v-model:value="formData.DoctorId" :autoload="true" />
</FormItem>
<FormItem label="取样流程" rule="Process">
<Textarea v-model:value="formData.Process" :auto-size="{ minRows: 3, maxRows: 10 }" />
</FormItem>
<FormItem label="启用状态">
<Switch v-model:checked="formData.Enabled" :data="OPTIONS.EnableDisable" />
</FormItem>
</Form>
</Drawer>
</template>

View File

@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Table } from '@/components';
import { pageData, useGridInit } from './page';
useGridInit();
</script>
<template>
<Table :page-data="pageData" />
</template>

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
import { Content } from '@/components';
import Grid from './grid.vue';
import Search from './search.vue';
import { pageData, usePageInit } from './page';
usePageInit();
//
const Editor = defineAsyncComponent(() => import('./editor.vue'));
</script>
<template>
<Content>
<Search />
<Grid />
</Content>
<component :is="Editor" v-if="pageData.editor?.visible" />
</template>

33
src/views/business/helper/model.d.ts vendored Normal file
View File

@ -0,0 +1,33 @@
/**
*
*/
export interface SamplingHelperEntity {
/**
* ID
*/
Id: string | null;
/**
*
*/
DoctorId: string | null;
/**
*
*/
LocationInfo?: { [key: string]: any };
/**
*
*/
Process: string | null;
/**
*
*/
Enabled: number;
/**
*
*/
CreateTime?: string | null;
/**
*
*/
UpdateTime?: string | null;
}

View File

@ -0,0 +1,120 @@
/**
*
* API操作
*/
import { usePageFactory, ApiUrls, ValidateRule } from '@skyfox2000/webbase';
import { ref } from 'vue';
export const SamplingHelperUrl: ApiUrls = {
/**
* api
*/
api: 'PLATFORM_API',
authorize: true,
urls: {
/**
*
*/
list: {
url: '/api/RCSamplingHelperSrv/list',
},
/**
*
*/
save: {
url: '/api/RCSamplingHelperSrv/save',
},
/**
*
*/
delete: {
url: '/api/RCSamplingHelperSrv/remove',
},
},
};
/**
*
*/
const defaultData: SamplingHelperEntity = {
Id: null,
DoctorId: null,
LocationInfo: {
LocationIds: [],
},
Process: null,
Enabled: 1,
CreateTime: null,
UpdateTime: null,
};
/**
*
* #
* # async-validator的语法规范
*/
const formRules: Record<string, ValidateRule> = {
DoctorId: {
required: true,
message: '医生不能为空',
},
Process: {
required: true,
message: '取样流程不能为空',
},
};
export const { editorData, gridData, pageData, usePageInit, useEditorInit, useGridInit } =
usePageFactory<SamplingHelperEntity>(SamplingHelperUrl, defaultData, formRules);
gridData.gridQuery = {
Option: {},
Query: {
$order: [['UpdateTime', 'desc']], // # 合适的默认查询条件,比如更新时间
},
};
/*
# CreateTime/UpdateTime
*/
const columns = ref([
{
title: '医生',
dataIndex: 'DoctorName',
width: 120,
responsive: ['md'],
},
{
title: '所在地区',
key: 'LocationInfo.LocationNames',
width: 120,
ellipsis: true,
customRender: ({ record }: { record: HospitalEntity }) => {
return record.LocationInfo?.LocationNames ? record.LocationInfo?.LocationNames.join('/') : '';
},
responsive: ['md'],
},
{
title: '取样流程',
dataIndex: 'Process',
width: 200,
responsive: ['md'],
},
{
title: '状态',
dataIndex: 'enabled',
width: 80,
responsive: ['md'],
},
{
title: '操作',
dataIndex: 'operation',
width: 100,
responsive: ['sm'],
},
]);
gridData.columns = columns.value;
gridData.tableSize = 'small';
gridData.remotePage = false;
gridData.tools = ['Reload', 'Fullscreen'];

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Search, SearchItem, Input } from '@/components';
import { pageData } from './page';
// #12
// #@/components
const searchData = ref<Record<string, any>>({
DoctorName: undefined,
});
</script>
<template>
<Search v-model:search="searchData" :page-data="pageData">
<SearchItem label="医生名">
<Input v-model:value="searchData.DoctorName" />
</SearchItem>
</Search>
</template>

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Drawer, Form, FormItem, Input, Textarea } from '@/components';
import { pageData, editorData } from './page';
import Status from './status.vue';
const formData = ref<OrderEntity>(editorData.formData);
</script>
<template>
<Drawer title="订单信息" :page-data="pageData">
<Form>
<FormItem label="会员名">
<Input v-model:value="formData.MemberName" />
</FormItem>
<FormItem label="产品名称">
<Input v-model:value="formData.ProductName" />
</FormItem>
<FormItem label="价码">
<Input v-model:value="formData.PriceCode" />
</FormItem>
<FormItem label="快递号">
<Input v-model:value="formData.ExpressNumber" />
</FormItem>
<FormItem label="订单状态" rule="Status">
<Status v-model:value="formData.Status" />
</FormItem>
<FormItem label="备注">
<Textarea v-model:value="formData.Remark" :auto-size="{ minRows: 3, maxRows: 6 }" />
</FormItem>
</Form>
</Drawer>
</template>

View File

@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Table } from '@/components';
import { pageData, useGridInit } from './page';
useGridInit();
</script>
<template>
<Table :page-data="pageData" />
</template>

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
import { Content } from '@/components';
import UploadForm from '@/components/content/dialog/uploadForm.vue';
import Grid from './grid.vue';
import Search from './search.vue';
import { pageData, usePageInit } from './page';
usePageInit();
//
const Editor = defineAsyncComponent(() => import('./editor.vue'));
</script>
<template>
<Content>
<Search />
<Grid />
</Content>
<component :is="Editor" v-if="pageData.editor?.visible" />
<UploadForm
v-if="pageData.subEditor!.uploadForm.visible"
width="600px"
:upload-form="pageData.subEditor!.uploadForm"
:max-count="1"
:page-data="pageData"
/>
</template>

65
src/views/business/order/model.d.ts vendored Normal file
View File

@ -0,0 +1,65 @@
/**
*
*/
export interface OrderEntity {
/**
* ID
*/
Id: string | null;
/**
*
*/
MemberId: string | null;
/**
*
*/
MemberName: string | null;
/**
* ID
*/
PriceId: string | null;
/**
*
*/
PriceCode: string | null;
/**
*
*/
ProductName: string | null;
/**
*
*/
Address: string | null;
/**
*
*/
ExpressNumber?: string | null;
/**
*
*/
ExpressInfo?: { [key: string]: any };
/**
*
*/
ReportFileUrl?: string | null;
/**
*
*/
Status: string | null;
/**
*
*/
Remark?: string | null;
/**
*
*/
Enabled: number;
/**
*
*/
CreateTime?: string | null;
/**
*
*/
UpdateTime?: string | null;
}

View File

@ -0,0 +1,186 @@
/**
*
* API操作
*/
import { usePageFactory, ApiUrls, ValidateRule, ButtonTool, exportSelectedRows, EditorData } from '@skyfox2000/webbase';
import { ref } from 'vue';
import message from 'vue-m-message';
export const OrderUrl: ApiUrls = {
/**
* api
*/
api: 'PLATFORM_API',
authorize: true,
urls: {
/**
*
*/
list: {
url: '/api/RCOrderSrv/list',
},
/**
*
*/
save: {
url: '/api/RCOrderSrv/save',
},
/**
*
*/
delete: {
url: '/api/RCOrderSrv/remove',
},
upload: {
url: '',
header: {
'Content-Type': 'multipart/form-data',
},
authorize: true, // 需要授权
},
},
};
/**
*
*/
const defaultData: OrderEntity = {
Id: null,
MemberId: null,
MemberName: null,
PriceId: null,
PriceCode: null,
ProductName: null,
Address: null,
ExpressNumber: null,
ExpressInfo: {},
ReportFileUrl: null,
Status: null,
Remark: null,
Enabled: 1,
CreateTime: null,
UpdateTime: null,
};
/**
*
* #
* # async-validator的语法规范
*/
const formRules: Record<string, ValidateRule> = {
Status: {
required: true,
message: '订单状态不能为空',
},
};
export const { editorData, gridData, pageData, usePageInit, useEditorInit, useGridInit } = usePageFactory<OrderEntity>(
OrderUrl,
defaultData,
formRules,
);
editorData.saveAsBtnVisible = false;
gridData.gridQuery = {
Option: {},
Query: {
$order: [['UpdateTime', 'desc']], // # 合适的默认查询条件,比如更新时间
},
};
/*
# CreateTime/UpdateTime
*/
const columns = ref([
{
title: '订单号',
dataIndex: 'OrderNo',
width: 120,
responsive: ['md'],
},
{
title: '会员名',
dataIndex: 'MemberName',
width: 120,
responsive: ['md'],
},
{
title: '手机号',
dataIndex: 'Mobile',
width: 120,
responsive: ['md'],
},
{
title: '价码',
dataIndex: 'PriceCode',
width: 100,
responsive: ['lg'],
},
{
title: '订单快递号',
dataIndex: 'ExpressNumber',
width: 120,
ellipsis: true,
responsive: ['lg'],
},
{
title: '订单状态',
dataIndex: 'Status',
width: 100,
responsive: ['md'],
},
{
title: '操作',
dataIndex: 'operation',
width: 150,
export: false,
responsive: ['sm'],
},
]);
gridData.columns = columns.value;
gridData.tableSize = 'small';
gridData.remotePage = false;
gridData.selectKeys = [];
gridData.selectRows = [];
gridData.selectable = true;
let download: ButtonTool = {
label: '下载',
key: 'download',
type: 'primary',
icon: 'icon-download',
click: () => {
if (gridData.selectRows && gridData.selectRows.length > 0) {
exportSelectedRows<OrderEntity>('订单-{YYYY-MM-DD}.csv', columns.value, gridData.selectRows);
} else {
message.warning('请选择需要下载的订单!');
}
},
};
gridData.buttons = ['New', download];
// 文件上传弹窗
let uploadForm: EditorData<OrderEntity> = {
visible: false,
formData: JSON.parse(JSON.stringify(defaultData)),
isFormSaving: false,
isFormLoading: false,
};
pageData.value.subEditor = {
uploadForm,
};
gridData.tools = ['Reload', 'Fullscreen'];
let upload: ButtonTool = {
label: '上传报告',
key: 'upload',
type: 'default',
icon: 'icon-upload',
click: (_, row) => {
pageData.value.subEditor!.uploadForm.formData = row;
pageData.value.subEditor!.uploadForm.visible = !pageData.value.subEditor!.uploadForm.visible;
},
};
gridData.operates = [upload, 'Edit', 'Delete'];

View File

@ -0,0 +1,36 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Search, SearchItem, Input } from '@/components';
import { pageData } from './page';
import Status from './status.vue';
// #12
// #@/components
const searchData = ref<Record<string, any>>({
OrderNo: null,
Mobile: null,
MemberName: null,
ProductName: null,
Status: null,
});
</script>
<template>
<Search v-model:search="searchData" :page-data="pageData">
<SearchItem label="订单号">
<Input v-model:value="searchData.OrderNo" />
</SearchItem>
<SearchItem label="手机号">
<Input v-model:value="searchData.Mobile" />
</SearchItem>
<SearchItem label="会员名">
<Input v-model:value="searchData.MemberName" />
</SearchItem>
<SearchItem label="产品名称">
<Input v-model:value="searchData.ProductName" />
</SearchItem>
<SearchItem label="订单状态">
<Status :all="true" v-model:value="searchData.Status" />
</SearchItem>
</Search>
</template>

View File

@ -0,0 +1,44 @@
<script setup lang="ts">
import { Select } from '@/components';
const props = defineProps({
all: {
type: Boolean,
default: false,
},
});
const options: Array<{
label: string;
value: string | null;
}> = [
{
label: '待付款',
value: '待付款',
},
{
label: '待发货',
value: '待发货',
},
{
label: '待收货',
value: '待收货',
},
{
label: '已完成',
value: '已完成',
},
{
label: '已取消',
value: '已取消',
},
];
if (props.all) {
options.unshift({
label: '全部',
value: null,
});
}
</script>
<template>
<Select :data="options"></Select>
</template>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Drawer, Form, FormItem, Input, Textarea, InputNumber, Switch } from '@/components';
import { pageData, editorData } from './page';
import { OPTIONS } from '@skyfox2000/webbase';
const formData = ref<PriceQREntity>(editorData.formData);
</script>
<template>
<Drawer title="价码信息" :page-data="pageData">
<Form>
<FormItem label="名称" rule="Name">
<Input v-model:value="formData.Name" />
</FormItem>
<FormItem label="价格码" rule="Code">
<Input v-model:value="formData.Code" />
</FormItem>
<FormItem label="单价" rule="Price">
<InputNumber v-model:value="formData.Price" />
</FormItem>
<FormItem label="备注">
<Textarea v-model:value="formData.Remark" :auto-size="{ minRows: 2, maxRows: 5 }" />
</FormItem>
<FormItem label="启用状态">
<Switch v-model:checked="formData.Enabled" :data="OPTIONS.EnableDisable" />
</FormItem>
</Form>
</Drawer>
</template>

View File

@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Table } from '@/components';
import { pageData, useGridInit } from './page';
useGridInit();
</script>
<template>
<Table :page-data="pageData" />
</template>

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
import { Content } from '@/components';
import Grid from './grid.vue';
import Search from './search.vue';
import { pageData, usePageInit } from './page';
usePageInit();
//
const Editor = defineAsyncComponent(() => import('./editor.vue'));
</script>
<template>
<Content>
<Search />
<Grid />
</Content>
<component :is="Editor" v-if="pageData.editor?.visible" />
</template>

37
src/views/business/priceqr/model.d.ts vendored Normal file
View File

@ -0,0 +1,37 @@
/**
*
*/
export interface PriceQREntity {
/**
* ID
*/
Id: string | null;
/**
*
*/
Code: string | null;
/**
*
*/
Name: string | null;
/**
*
*/
Price: number | null;
/**
*
*/
Remark?: string | null;
/**
*
*/
Enabled: number;
/**
*
*/
CreateTime?: string | null;
/**
*
*/
UpdateTime?: string | null;
}

View File

@ -0,0 +1,124 @@
/**
*
* API操作
*/
import { usePageFactory, ApiUrls, ValidateRule } from '@skyfox2000/webbase';
import { ref } from 'vue';
export const PriceQRUrl: ApiUrls = {
/**
* api
*/
api: 'PLATFORM_API',
authorize: true,
urls: {
/**
*
*/
find: {
url: '/api/RCPriceQRSrv/find',
},
/**
*
*/
save: {
url: '/api/RCPriceQRSrv/save',
},
/**
*
*/
delete: {
url: '/api/RCPriceQRSrv/remove',
},
},
};
/**
*
*/
const defaultData: PriceQREntity = {
Id: null,
Code: null,
Name: null,
Price: null,
Remark: null,
Enabled: 1,
CreateTime: null,
UpdateTime: null,
};
/**
*
* #
* # async-validator的语法规范
*/
const formRules: Record<string, ValidateRule> = {
Code: {
required: true,
message: '价格码不能为空',
},
Name: {
required: true,
message: '名称不能为空',
},
Price: {
required: true,
message: '单价不能为空',
},
};
export const { editorData, gridData, pageData, usePageInit, useEditorInit, useGridInit } =
usePageFactory<PriceQREntity>(PriceQRUrl, defaultData, formRules);
gridData.gridQuery = {
Option: {},
Query: {
$order: [['UpdateTime', 'desc']], // # 合适的默认查询条件,比如更新时间
},
};
/*
# CreateTime/UpdateTime
*/
const columns = ref([
{
title: '名称',
dataIndex: 'Name',
width: 150,
responsive: ['md'],
},
{
title: '价格码',
dataIndex: 'Code',
width: 120,
responsive: ['md'],
},
{
title: '单价',
dataIndex: 'Price',
width: 100,
responsive: ['md'],
},
{
title: '备注',
dataIndex: 'Remark',
width: 150,
responsive: ['md'],
},
{
title: '状态',
dataIndex: 'enabled',
width: 80,
responsive: ['md'],
},
{
title: '操作',
dataIndex: 'operation',
width: 100,
responsive: ['sm'],
},
]);
gridData.columns = columns.value;
gridData.tableSize = 'small';
gridData.tools = ['Reload', 'Fullscreen'];

View File

@ -0,0 +1,23 @@
<script setup lang="ts">
import { ref } from "vue";
import { Search, SearchItem, Input } from "@/components";
import { pageData } from "./page";
// #12
// #@/components
const searchData = ref<Record<string, any>>({
Code: undefined,
Name: undefined,
});
</script>
<template>
<Search v-model:search="searchData" :page-data="pageData">
<SearchItem label="价格码">
<Input v-model:value="searchData.Code" />
</SearchItem>
<SearchItem label="名称">
<Input v-model:value="searchData.Name" />
</SearchItem>
</Search>
</template>

49
src/views/login/index.vue Normal file
View File

@ -0,0 +1,49 @@
<script setup lang="ts">
import { ref } from 'vue';
import LoginBox from './login.vue';
import { HostInfo } from '@skyfox2000/microbase';
import { getHostInfo } from '@skyfox2000/webbase';
const hostInfo = ref<HostInfo>({
Host: '',
Title: '管理系统',
FullTitle: '管理系统',
Provider: 'XXXX有限公司',
});
hostInfo.value = getHostInfo();
document.title = '用户登录-' + hostInfo.value.Title;
</script>
<template>
<div class="bg-white text-[#424242]">
<!-- Header -->
<header
class="w-full h-[100px] bg-gradient-to-r from-[#618dde] to-[#e3f1ff] flex justify-start items-center pl-[50px]"
>
<div class="text-[28px] text-white">{{ hostInfo.Title }}</div>
</header>
<!-- Content -->
<main
class="w-full bg-[#e3f1ff] border-t-4 border-b-4 border-[#769BDA] flex justify-end items-center min-h-[calc(100vh-200px)] bg-no-repeat bg-[url('@/assets/images/login/banner.png')] bg-[25%_center] bg-[length:600px_auto]"
>
<div class="w-full pr-[9%]">
<div class="mx-auto max-w-[1280px] flex justify-end">
<LoginBox :title="hostInfo.Title" />
</div>
</div>
</main>
<!-- Footer -->
<footer class="bg-[#f5f5f5]/90 w-full min-h-[100px] flex justify-start items-center">
<div class="w-full flex justify-around items-center">
<div class="w-1/3"></div>
<div class="w-1/3 text-center text-gray-500">
© 2023 {{ hostInfo.FullTitle }} - {{ hostInfo.Provider }} 提供技术支持
</div>
<div class="w-1/3"></div>
</div>
</footer>
</div>
</template>

151
src/views/login/login.vue Normal file
View File

@ -0,0 +1,151 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import Message from 'vue-m-message';
import { Input, InputPassword } from '@/components';
import { userLogin } from '@skyfox2000/webbase';
defineProps({
title: String,
});
const onLoginClicked = ref(userLogin);
const name = ref('');
const password = ref('');
const nameTip = ref('请输入用户名');
const nameStatus = ref<'' | 'error' | 'warning' | undefined>('');
const passwordTip = ref('请输入密码');
const passwordStatus = ref<'' | 'error' | 'warning' | undefined>('');
watch(name, () => {
nameStatus.value = '';
nameTip.value = '请输入用户名';
});
watch(password, () => {
passwordStatus.value = '';
passwordTip.value = '请输入密码';
});
const logining = ref<boolean>(false);
const onClicked = async () => {
nameTip.value = '请输入用户名';
nameStatus.value = '';
passwordTip.value = '请输入密码';
passwordStatus.value = '';
if (name.value === '') {
nameTip.value = '用户名不能为空';
nameStatus.value = 'error';
}
if (password.value === '') {
passwordTip.value = '密码不能为空';
passwordStatus.value = 'error';
}
if (name.value !== '' && password.value !== '') {
if (onLoginClicked.value) {
logining.value = true;
const result = await onLoginClicked.value({
UserName: name.value,
UserPass: password.value,
});
setTimeout(() => {
logining.value = false;
}, 1500);
if (result && result.errno) {
nameTip.value = result.msg!;
nameStatus.value = 'error';
passwordTip.value = result.msg!;
passwordStatus.value = 'error';
}
} else {
Message.error('未配置登录接口!');
}
}
};
</script>
<template>
<div
class="w-[300px] h-[343px] flex mt-[2px] border-[3px] border-blue-300 rounded-[10px] shadow-lg flex-col items-center bg-white px-[16px]"
>
<div class="w-[84%]">
<div class="text-center text-gray-500 font-medium mt-[30px] mb-[22px]">{{ title }} 用户登录</div>
</div>
<div class="w-[84%] h-[74px]">
<Input placeholder="请输入用户名" v-model:value="name" :status="nameStatus" :maxlength="30">
<template #prefix>
<i class="iconfont icon-user text-gray-400" :class="[nameStatus === 'error' ? 'text-red-400' : '']"></i>
</template>
</Input>
<span class="mt-1 hidden" :class="[nameStatus]">{{ nameTip }}</span>
</div>
<div class="w-[84%] h-[70px]">
<InputPassword placeholder="请输入密码" v-model:value="password" :status="passwordStatus" :maxlength="30">
<template #prefix>
<i
class="iconfont icon-mima text-gray-400"
:class="[passwordStatus === 'error' ? 'text-red-400' : '']"
></i>
</template>
</InputPassword>
<span class="mt-1 hidden" :class="[passwordStatus]">{{ passwordTip }}</span>
</div>
<div class="w-[84%] h-[34px]">
<!-- <Col>忘记密码</Col> -->
</div>
<div class="w-[84%]">
<button
:class="[
'w-full',
'border-[2px]',
'border-solid',
'border-[#90B0E3] hover:border-[#769BDA]',
'bg-[#769BDA]' /* 替换为接近 #4476D3 的 Tailwind 颜色 */,
'focus:ring-4',
'focus:ring-blue-300',
'text-base' /* 调整文字大小 */,
'font-normal' /* 调整文字粗细为正常 */,
'text-white',
'text-center',
'py-2',
'px-4',
'rounded-xl' /* 增加圆角 */,
'disabled:bg-gray-300',
'disabled:cursor-not-allowed',
logining ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-[#90B0E3]',
]"
@click="onClicked"
:disabled="logining"
>
<i class="anticon-spin iconfont icon-loading loading" v-if="logining"></i>
<span class="ml-[10px]"> </span>
</button>
</div>
</div>
</template>
<style scoped lang="less">
.loading {
color: #fff;
font-size: 17px;
animation: loading 2s infinite linear;
}
@keyframes loading {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.error {
display: block;
color: #ff0000;
font-size: 12px;
margin-left: 14px;
}
</style>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Drawer, Form, FormItem, Input, Textarea, InputPassword, Switch } from '@/components';
import { pageData, editorData } from './page';
import { OPTIONS } from '@skyfox2000/webbase';
const formData = ref<AccountEntity>(editorData.formData);
</script>
<template>
<Drawer title="账号信息" :page-data="pageData">
<Form>
<FormItem label="登录账号" rule="UserCode">
<Input v-model:value="formData.UserCode" />
</FormItem>
<FormItem label="姓名" rule="Name">
<Input v-model:value="formData.Name" />
</FormItem>
<FormItem label="密码" rule="UserPwd">
<InputPassword v-model:value="formData.UserPwd" />
</FormItem>
<FormItem label="备注">
<Textarea v-model:value="formData.Remark" :auto-size="{ minRows: 2, maxRows: 5 }" />
</FormItem>
<FormItem label="启用状态">
<Switch v-model:checked="formData.Enabled" :data="OPTIONS.EnableDisable" />
</FormItem>
</Form>
</Drawer>
</template>

View File

@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Table } from '@/components';
import { pageData, useGridInit } from './page';
useGridInit();
</script>
<template>
<Table :page-data="pageData" />
</template>

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
import { Content } from '@/components';
import Grid from './grid.vue';
import Search from './search.vue';
import { pageData, usePageInit } from './page';
usePageInit();
//
const Editor = defineAsyncComponent(() => import('./editor.vue'));
</script>
<template>
<Content>
<Search />
<Grid />
</Content>
<component :is="Editor" v-if="pageData.editor?.visible" />
</template>

73
src/views/system/account/model.d.ts vendored Normal file
View File

@ -0,0 +1,73 @@
/**
*
*/
export interface AccountEntity {
/**
* Admin管理员/User操作员
*/
AccountType: string | null;
/**
*
*/
ClientId?: string | null;
/**
*
*/
DepartId?: string | null;
/**
*
*/
Enabled: number;
/**
* Id
*/
Id: string | null;
/**
*
*/
JobTitleId?: string | null;
/**
*
*/
Name: string | null;
/**
*
*/
PassTime?: string | null;
/**
*
*/
Remark?: string | null;
/**
*
*/
State?: string | null;
/**
*
*/
Tags?: string[];
/**
*
*/
UpdateTime?: string | null;
/**
*
*/
UpdateUser?: string | null;
/**
*
*/
UserCode: string | null;
/**
* (Platform平台/Client租户/Member会员)
*/
UserLevel: string | null;
/**
*
*/
UserProps?: { [key: string]: any };
/**
*
*/
UserPwd?: string | null;
}

View File

@ -0,0 +1,127 @@
/**
*
* API操作
*/
import { usePageFactory, ApiUrls, ValidateRule } from '@skyfox2000/webbase';
import { ref } from 'vue';
export const AccountUrl: ApiUrls = {
/**
* api
*/
api: 'PLATFORM_API',
authorize: true,
urls: {
/**
*
*/
find: {
url: '/api/RCAccountOpSrv/find',
},
/**
*
*/
save: {
url: '/api/RCAccountOpSrv/save',
},
/**
*
*/
delete: {
url: '/api/RCAccountOpSrv/remove',
},
},
};
/**
*
*/
const defaultData: AccountEntity = {
Id: null,
ClientId: null,
UserCode: null,
UserPwd: null,
Name: null,
AccountType: 'Admin',
UserLevel: 'Platform',
JobTitleId: null,
DepartId: null,
Tags: [],
UserProps: {},
PassTime: null,
Remark: null,
State: null,
Enabled: 1,
UpdateTime: null,
UpdateUser: null,
};
/**
*
* #
* # async-validator的语法规范
*/
const formRules: Record<string, ValidateRule> = {
UserCode: {
required: true,
message: '登录账号不能为空',
},
Name: {
required: true,
message: '姓名不能为空',
},
UserPwd: {
required: true,
message: '密码不能为空',
},
};
export const { editorData, gridData, pageData, usePageInit, useEditorInit, useGridInit } =
usePageFactory<AccountEntity>(AccountUrl, defaultData, formRules);
gridData.gridQuery = {
Option: {},
Query: {
$order: [['UpdateTime', 'desc']], // # 合适的默认查询条件,比如更新时间
},
};
/*
# CreateTime/UpdateTime
*/
const columns = ref([
{
title: '登录账号',
dataIndex: 'UserCode',
width: 100,
responsive: ['md'],
},
{
title: '姓名',
dataIndex: 'Name',
width: 100,
responsive: ['md'],
},
{
title: '备注',
dataIndex: 'Remark',
width: 100,
responsive: ['md'],
},
{
title: '状态',
dataIndex: 'enabled',
width: 80,
responsive: ['md'],
},
{
title: '操作',
dataIndex: 'operation',
width: 100,
responsive: ['sm'],
},
]);
gridData.columns = columns.value;
gridData.tableSize = 'small';
gridData.tools = ['Reload', 'Fullscreen'];

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import { ref } from "vue";
import { Search, SearchItem, Input } from "@/components";
import { pageData } from "./page";
const searchData = ref<Record<string, any>>({
UserCode: undefined,
Name: undefined,
});
</script>
<template>
<Search v-model:search="searchData" :page-data="pageData">
<SearchItem label="登录账号">
<Input v-model:value="searchData.UserCode" />
</SearchItem>
<SearchItem label="姓名">
<Input v-model:value="searchData.Name" />
</SearchItem>
</Search>
</template>

View File

@ -0,0 +1,23 @@
<script setup lang="ts">
import { AutoComplete } from '@/components';
import { DoctorUrl } from './page';
import { ReqParams } from '@skyfox2000/fapi';
const onSearch = (search_value: String, query: ReqParams) => {
query.Query = {
Name: '%' + search_value + '%',
};
};
</script>
<template>
<AutoComplete
:onsearch="onSearch"
:url="DoctorUrl.urls.select"
:params="{
Option: {
SelectFields: ['S_Doctor.Id', 'S_Doctor.Name'],
},
}"
/>
</template>

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Drawer, Form, FormItem, Input, Switch } from '@/components';
import { pageData, editorData } from './page';
import { OPTIONS } from '@skyfox2000/webbase';
import HospitalSelect from '@/views/system/hospital/select.vue';
const formData = ref<DoctorEntity>(editorData.formData);
</script>
<template>
<Drawer title="医生信息" :page-data="pageData">
<Form>
<FormItem label="姓名" rule="Name">
<Input v-model:value="formData.Name" />
</FormItem>
<FormItem label="手机号" rule="Mobile">
<Input v-model:value="formData.Mobile" />
</FormItem>
<FormItem label="所属医院" rule="HospitalId">
<HospitalSelect v-model:value="formData.HospitalId" />
</FormItem>
<FormItem label="启用状态">
<Switch v-model:checked="formData.Enabled" :data="OPTIONS.EnableDisable" />
</FormItem>
</Form>
</Drawer>
</template>

View File

@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Table } from '@/components';
import { pageData, useGridInit } from './page';
useGridInit();
</script>
<template>
<Table :page-data="pageData" />
</template>

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
import { Content } from '@/components';
import Grid from './grid.vue';
import Search from './search.vue';
import { pageData, usePageInit } from './page';
usePageInit();
//
const Editor = defineAsyncComponent(() => import('./editor.vue'));
</script>
<template>
<Content>
<Search />
<Grid />
</Content>
<component :is="Editor" v-if="pageData.editor?.visible" />
</template>

33
src/views/system/doctor/model.d.ts vendored Normal file
View File

@ -0,0 +1,33 @@
/**
*
*/
export interface DoctorEntity {
/**
* ID
*/
Id: string | null;
/**
*
*/
Mobile: string | null;
/**
*
*/
Name: string | null;
/**
*
*/
HospitalId: string | null;
/**
*
*/
Enabled: number;
/**
*
*/
CreateTime?: string | null;
/**
*
*/
UpdateTime?: string | null;
}

View File

@ -0,0 +1,125 @@
/**
*
* API操作
*/
import { usePageFactory, ApiUrls, ValidateRule } from '@skyfox2000/webbase';
import { ref } from 'vue';
export const DoctorUrl: ApiUrls = {
/**
* api
*/
api: 'PLATFORM_API',
authorize: true,
urls: {
/**
*
*/
list: {
url: '/api/RCDoctorSrv/list',
},
select: {
url: '/api/RCDoctorSrv/select',
cacheTime: 600, // 缓存600秒
},
/**
*
*/
save: {
url: '/api/RCDoctorSrv/save',
},
/**
*
*/
delete: {
url: '/api/RCDoctorSrv/remove',
},
},
};
/**
*
*/
const defaultData: DoctorEntity = {
Id: null,
Mobile: null,
Name: null,
HospitalId: null,
Enabled: 1,
CreateTime: null,
UpdateTime: null,
};
/**
*
* #
* # async-validator的语法规范
*/
const formRules: Record<string, ValidateRule> = {
Mobile: {
required: true,
message: '手机号不能为空',
},
Name: {
required: true,
message: '姓名不能为空',
},
HospitalId: {
required: true,
message: '所属医院不能为空',
},
};
export const { editorData, gridData, pageData, usePageInit, useEditorInit, useGridInit } = usePageFactory<DoctorEntity>(
DoctorUrl,
defaultData,
formRules,
);
gridData.gridQuery = {
Option: {},
Query: {
$order: [['UpdateTime', 'desc']], // # 合适的默认查询条件,比如更新时间
},
};
/*
# CreateTime/UpdateTime
*/
const columns = ref([
{
title: '姓名',
dataIndex: 'Name',
width: 100,
responsive: ['md'],
},
{
title: '手机号',
dataIndex: 'Mobile',
width: 120,
responsive: ['md'],
},
{
title: '所属医院',
dataIndex: 'HospitalName',
width: 150,
responsive: ['md'],
},
{
title: '状态',
dataIndex: 'enabled',
width: 80,
responsive: ['md'],
},
{
title: '操作',
dataIndex: 'operation',
width: 100,
responsive: ['sm'],
},
]);
gridData.columns = columns.value;
gridData.tableSize = 'small';
gridData.remotePage = false;
gridData.tools = ['Reload', 'Fullscreen'];

View File

@ -0,0 +1,23 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Search, SearchItem, Input } from '@/components';
import { pageData } from './page';
// #12
// #@/components
const searchData = ref<Record<string, any>>({
Mobile: undefined,
Name: undefined,
});
</script>
<template>
<Search v-model:search="searchData" :page-data="pageData">
<SearchItem label="手机号">
<Input v-model:value="searchData.Mobile" />
</SearchItem>
<SearchItem label="姓名">
<Input v-model:value="searchData.Name" />
</SearchItem>
</Search>
</template>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import { Select } from '@/components';
import { DoctorUrl } from './page';
</script>
<template>
<Select
:url="DoctorUrl.urls.list"
:params="{
Option: {
SelectFields: ['Id', 'Name'],
},
}"
></Select>
</template>

Some files were not shown because too many files have changed in this diff Show More