Browse Source

feat: 客户端模块提交

master
AaronWu 1 year ago
parent
commit
c23be32e3c
  1. 2
      .env.production
  2. 11
      components.d.ts
  3. 3
      package.json
  4. 4
      src/api/issue/model.d.ts
  5. 2
      src/api/user/model.d.ts
  6. BIN
      src/assets/icons/logo.png
  7. BIN
      src/assets/images/background.jpg
  8. 1
      src/enums/dictEnum.ts
  9. 4
      src/layout/logo/index.vue
  10. 6
      src/router/constant.ts
  11. 24
      src/router/outsideLayout.ts
  12. 2
      src/store/modules/projectConfig.ts
  13. 333
      src/views/client/entrance/index.vue
  14. 249
      src/views/client/issue/index.vue
  15. 165
      src/views/client/knowledgeBase/index.vue
  16. 179
      src/views/knowledgeBase/index.vue
  17. 113
      src/views/login/index.vue
  18. 8
      src/views/question/issue/columns.tsx
  19. 14
      src/views/question/issue/data.ts
  20. 43
      src/views/question/issue/detail.vue
  21. 20
      src/views/question/issue/formSchemas.tsx
  22. 47
      src/views/question/issue/index.vue
  23. 8
      src/views/question/knowledge/columns.tsx
  24. 39
      src/views/question/knowledge/formSchemas.tsx
  25. 97
      src/views/question/knowledge/index.vue
  26. 1
      src/views/system/prodVersion/Tree.vue
  27. 11
      src/views/system/user/columns.tsx
  28. 18
      src/views/system/user/formSchemas.ts
  29. 7
      vite.config.ts
  30. 100
      yarn.lock

2
.env.production

@ -9,7 +9,7 @@
# 只在生产模式中被载入
# 网站前缀
VITE_BASE_API_URL = http://192.168.3.118:10010/server/
VITE_BASE_API_URL = http://43.137.2.78:8082/server/
# base api
VITE_BASE_API = '/server/'

11
components.d.ts

@ -9,16 +9,27 @@ declare module '@vue/runtime-core' {
export interface GlobalComponents {
AButton: typeof import('ant-design-vue/es')['Button']
ACard: typeof import('ant-design-vue/es')['Card']
ACardMeta: typeof import('ant-design-vue/es')['CardMeta']
ACol: typeof import('ant-design-vue/es')['Col']
AEmpty: typeof import('ant-design-vue/es')['Empty']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AInput: typeof import('ant-design-vue/es')['Input']
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
ALayout: typeof import('ant-design-vue/es')['Layout']
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
ApiSelect: typeof import('./src/components/core/schema-form/src/components/ApiSelect.vue')['default']
ARow: typeof import('ant-design-vue/es')['Row']
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin']
AStep: typeof import('ant-design-vue/es')['Step']
ASteps: typeof import('ant-design-vue/es')['Steps']
ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATree: typeof import('ant-design-vue/es')['Tree']
ATypographyText: typeof import('ant-design-vue/es')['TypographyText']
BasicArrow: typeof import('./src/components/basic/basic-arrow/index.vue')['default']
BasicDrawer: typeof import('./src/components/core/Drawer/src/BasicDrawer.vue')['default']

3
package.json

@ -45,6 +45,7 @@
"vite": "^4.3.2",
"vite-plugin-svg-icons": "~2.0.1",
"vite-tsconfig-paths": "^4.2.0",
"vue-tsc": "^1.4.2"
"vue-tsc": "^1.4.2",
"vite-plugin-top-level-await": "^1.5.0"
}
}

4
src/api/issue/model.d.ts

@ -24,6 +24,8 @@ declare namespace API {
updateUserid?: number; // 更新人ID
version?: string; // 版本
fileList?: any[]; // 附件列表
tags?: string[] | string; // 标签
solution?: string; // 解决方案
};
type CreateIssueParams = {
@ -47,6 +49,8 @@ declare namespace API {
updateUserid?: number; // 更新人ID
version?: string; // 版本
fileList?: any[]; // 附件列表
tags?: string[] | string; // 标签
solution?: string; // 解决方案
};
type DeleteIssueParams = {

2
src/api/user/model.d.ts

@ -19,6 +19,7 @@ declare namespace API {
remark: string;
createTime: string;
isAdmin: number;
email: string;
pendingStatus?: boolean;
};
@ -31,6 +32,7 @@ declare namespace API {
state: number;
remark: string;
isAdmin: number;
email: string;
};
type DeleteUserParams = {

BIN
src/assets/icons/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
src/assets/images/background.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

1
src/enums/dictEnum.ts

@ -1,3 +1,4 @@
export enum DictEnum {
QUESTION_TYPE = '问题属性',
TAG_TYPE = '标签',
}

4
src/layout/logo/index.vue

@ -54,7 +54,7 @@
.title-text {
font-size: 18px;
font-weight: 600;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: @primary-color;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: 1px;
@ -64,7 +64,7 @@
height: 2px;
width: 24px;
margin-top: 4px;
background: linear-gradient(90deg, #667eea, #764ba2);
background: @primary-color;
transition: width 0.3s ease;
}

6
src/router/constant.ts

@ -2,6 +2,10 @@ export const LOGIN_NAME = 'Login';
export const KNOWLEDGE_NAME = 'Knowledge';
export const ISSUE_NAME = 'issue';
export const ENTRANCE_NAME = 'entrance';
export const REDIRECT_NAME = 'Redirect';
export const PARENT_LAYOUT_NAME = 'ParentLayout';
@ -13,4 +17,4 @@ export const whiteNameList = [LOGIN_NAME, 'icons', 'error', 'error-404'] as cons
export type WhiteNameList = typeof whiteNameList;
export type WhiteName = typeof whiteNameList[number];
export type WhiteName = (typeof whiteNameList)[number];

24
src/router/outsideLayout.ts

@ -1,5 +1,5 @@
import type { RouteRecordRaw } from 'vue-router';
import { LOGIN_NAME, KNOWLEDGE_NAME } from '@/router/constant';
import { LOGIN_NAME, KNOWLEDGE_NAME, ISSUE_NAME, ENTRANCE_NAME } from '@/router/constant';
/**
* layout布局之外的路由
@ -16,10 +16,28 @@ export const LoginRoute: RouteRecordRaw = {
export const KnowledgeRoute: RouteRecordRaw = {
path: '/knowledge',
name: KNOWLEDGE_NAME,
component: () => import(/* webpackChunkName: "login" */ '@/views/knowledgeBase/index.vue'),
component: () => import(/* webpackChunkName: "login" */ '@/views/client/knowledgeBase/index.vue'),
meta: {
title: '知识库',
},
};
export default [LoginRoute, KnowledgeRoute];
export const Issue: RouteRecordRaw = {
path: '/issue',
name: ISSUE_NAME,
component: () => import(/* webpackChunkName: "login" */ '@/views/client/issue/index.vue'),
meta: {
title: '问题工单',
},
};
export const Entrance: RouteRecordRaw = {
path: '/entrance',
name: ENTRANCE_NAME,
component: () => import(/* webpackChunkName: "login" */ '@/views/client/entrance/index.vue'),
meta: {
title: '入口',
},
};
export default [LoginRoute, KnowledgeRoute, Issue, Entrance];

2
src/store/modules/projectConfig.ts

@ -60,7 +60,7 @@ export type ThemeState = {
export const defaultConfig: ThemeState = {
navTheme: 'dark', // theme for nav menu
primaryColor: 'rgb(24, 144, 255)', // '#F5222D', // primary color of ant design
primaryColor: 'rgb(250, 84, 28)', // '#F5222D', // primary color of ant design
layout: 'sidemenu', // nav menu position: `sidemenu` or `topmenu`
contentWidth: 'Fluid', // layout of content: `Fluid` or `Fixed`, only works when layout is topmenu
fixedHeader: false, // sticky header

333
src/views/client/entrance/index.vue

@ -0,0 +1,333 @@
<template>
<div class="entrance-container">
<!-- 顶部导航 -->
<div class="header">
<div class="logo">
<!-- <img src="http://193.112.23.48:88/techscan/assets/onepage/img/logo/techscanlogo.png" /> -->
<span>太迅条码帮助中心</span>
</div>
<div class="nav-links">
<a-space>
<a href="#" class="nav-item">返回官网</a>
<!-- <a href="#" class="nav-item">问题社区</a> -->
<!-- <a href="#" class="nav-item">更新日志</a> -->
<a-button type="text" class="favorite">
<star-outlined />
</a-button>
</a-space>
</div>
</div>
<!-- 主要内容区 -->
<div class="main-content">
<!-- 快捷入口卡片 -->
<div class="quick-access" v-if="false">
<a-row :gutter="16">
<a-col :span="8">
<a-card class="access-card">
<template #cover>
<user-outlined class="card-icon" />
</template>
<a-card-meta title="加入社区" description="发布问题和分享经验,参与讨论" />
</a-card>
</a-col>
<a-col :span="8">
<a-card class="access-card">
<template #cover>
<clock-circle-outlined class="card-icon" />
</template>
<a-card-meta title="疑点帮助" description="获得即时工作答复和问题解答" />
</a-card>
</a-col>
<a-col :span="8">
<a-card class="access-card">
<template #cover>
<mail-outlined class="card-icon" />
</template>
<a-card-meta title="发送邮件" description="to: support@baklib.jp" />
</a-card>
</a-col>
</a-row>
</div>
<!-- 搜索框 -->
<div class="search-section">
<h1>👋 有什么可以帮你?</h1>
<div class="search-wrapper">
<a-input-search
v-model:value="searchText"
placeholder="在这里搜索"
size="large"
@search="onSearch"
class="rounded-search"
/>
</div>
<div class="search-tags">
<a-tag v-for="tag in searchTags" :key="tag">{{ tag }}</a-tag>
</div>
</div>
<!-- 新手入门区域 -->
<div class="getting-started">
<a-row :gutter="[24, 24]">
<a-col :span="12" v-for="item in startItems" :key="item.title">
<div class="start-card">
<div class="card-content">
<div class="icon-wrapper">
<component :is="item.icon" class="card-icon" />
</div>
<div class="text-content">
<h3>{{ item.title }}</h3>
<p>{{ item.description }}</p>
</div>
</div>
<div class="card-action">
<a-button type="link" @click="goTo(item.route)">立即进入 </a-button>
</div>
</div>
</a-col>
</a-row>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import {
StarOutlined,
UserOutlined,
ClockCircleOutlined,
MailOutlined,
FormOutlined,
BookOutlined,
} from '@ant-design/icons-vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const searchText = ref('');
const searchTags = ref(['快捷指南', 'KB快捷键', 'DAM', 'CMS', 'Wiki', 'Community']);
const startItems = ref([
{
title: '问题工单',
description: '提交问题和请求,跟踪进度',
icon: FormOutlined,
route: '/issue',
},
{
title: '知识库',
description: '整理组织知识,构建统一内容中心,服务团队',
icon: BookOutlined,
route: '/knowledge',
},
]);
const goTo = (route: string) => {
console.log(`前往 ${route}`);
router.push(route);
};
const onSearch = (value: string) => {
console.log('搜索:', value);
};
</script>
<style lang="less" scoped>
.entrance-container {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
.header {
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
.logo {
display: flex;
align-items: center;
gap: 8px;
img {
height: 32px;
}
span {
font-size: 18px;
font-weight: 500;
}
}
.nav-links {
.nav-item {
color: rgba(0, 0, 0, 0.85);
padding: 0 16px;
&:hover {
color: #1890ff;
}
}
}
}
.main-content {
max-width: 1200px;
margin: 0 auto;
padding: 40px 24px;
.quick-access {
margin-bottom: 48px;
.access-card {
text-align: center;
border-radius: 12px;
overflow: hidden;
transition: all 0.3s;
&:hover {
transform: translateY(-5px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
}
.card-icon {
font-size: 36px;
padding: 24px 0;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: white;
}
}
}
.search-section {
text-align: center;
margin-bottom: 48px;
h1 {
font-size: 32px;
margin-bottom: 24px;
}
.ant-input-search {
max-width: 600px;
margin: 0 auto;
}
.search-tags {
margin-top: 16px;
.ant-tag {
margin: 4px;
cursor: pointer;
&:hover {
color: #1890ff;
}
}
}
}
.getting-started {
.start-card {
background: white;
padding: 24px;
border-radius: 16px;
transition: all 0.3s ease;
border: 1px solid rgba(0, 0, 0, 0.06);
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
&:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.08);
border-color: transparent;
}
.card-content {
display: flex;
align-items: flex-start;
gap: 20px;
}
.icon-wrapper {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
.card-icon {
font-size: 24px;
color: white;
}
}
.text-content {
flex: 1;
h3 {
font-size: 20px;
margin-bottom: 8px;
color: #1f1f1f;
font-weight: 600;
}
p {
color: rgba(0, 0, 0, 0.65);
margin: 0;
line-height: 1.6;
}
}
.card-action {
margin-top: 20px;
text-align: right;
.ant-btn {
padding: 0;
font-size: 16px;
&:hover {
transform: translateX(5px);
}
}
}
}
}
}
}
.search-wrapper {
max-width: 600px;
margin: 0 auto;
.rounded-search {
:deep(.ant-input) {
border-radius: 24px 0 0 24px;
height: 48px;
padding-left: 24px;
border: 1px solid #d9d9d9;
&:focus, &:hover {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
}
}
:deep(.ant-input-group-addon) {
background:none;
.ant-btn {
border-radius: 0 24px 24px 0;
height: 48px;
}
}
}
}
</style>

249
src/views/client/issue/index.vue

@ -0,0 +1,249 @@
<template>
<div class="knowledge-base">
<a-row :gutter="16">
<!-- 左侧目录树 -->
<a-col :span="6">
<a-card class="tree-card">
<template #title>
<span class="card-title"> <folder-outlined /> 问题工单目录 </span>
</template>
<a-tree
v-model:selectedKeys="selectedKeys"
v-model:expandedKeys="expandedKeys"
:tree-data="treeData"
:defaultExpandAll="expandAll"
@select="onSelect"
>
<template #switcherIcon="{ switcherCls }"
><down-outlined :class="switcherCls"
/></template>
</a-tree>
</a-card>
</a-col>
<!-- 右侧内容区 -->
<a-col :span="18">
<a-card class="content-card">
<template #title>
<div class="flex justify-between items-start">
<span class="card-title"> <book-outlined /> 问题工单内容 </span>
<!-- 快捷操作区域 -->
<a-space>
<a-button
type="primary"
v-for="action in quickActions"
:key="action.title"
@click="action.onClick"
>
<template #icon><component :is="action.icon" /></template>
{{ action.title }}
</a-button>
</a-space>
</div>
</template>
<!-- 内容展示区域 -->
<div class="content-area">
<template v-if="selectedKeys.length">
<!-- 这里可以根据选中的目录显示具体内容 -->
<Detail :id="curRow.id" />
</template>
<template v-else>
<a-empty description="请选择左侧目录" />
</template>
</div>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import {
FolderOutlined,
BookOutlined,
FileTextOutlined,
TeamOutlined,
SettingOutlined,
PlusOutlined,
EditOutlined,
DeleteOutlined,
ExportOutlined,
DownOutlined,
} from '@ant-design/icons-vue';
import { fetchProdList, fetchVersionPageList } from '@/api/prodVersion';
import { fetchIssuePageList } from '@/api/issue';
import Detail from '@/views/question/issue/detail.vue';
//
const treeData = ref<any[]>([]);
const expandAll = ref(false);
//
const selectedKeys = ref<string[]>([]);
const expandedKeys = ref<string[]>(['1', '2']);
const curRow = ref<any>({});
//
const featureCards = ref([
{
title: '文档管理',
description: '集中管理所有知识文档',
icon: FileTextOutlined,
color: '#1890ff',
},
{
title: '团队协作',
description: '支持多人协同编辑',
icon: TeamOutlined,
color: '#52c41a',
},
{
title: '系统设置',
description: '自定义问题工单配置',
icon: SettingOutlined,
color: '#722ed1',
},
]);
//
const quickActions = ref([
{
title: '新建文档',
icon: PlusOutlined,
onClick: () => console.log('新建文档'),
},
{
title: '编辑文档',
icon: EditOutlined,
onClick: () => console.log('编辑文档'),
},
{
title: '删除文档',
icon: DeleteOutlined,
onClick: () => console.log('删除文档'),
},
{
title: '导出文档',
icon: ExportOutlined,
onClick: () => console.log('导出文档'),
},
]);
//
const onSelect = async (selectedKeys: string[], info: any) => {
console.log('selected', selectedKeys, info);
const {
node: { isLeaf },
} = info;
//
if (isLeaf) {
//
console.log('选中叶子节点:', selectedKeys[0]);
curRow.value = {
id: selectedKeys[0],
};
} else {
console.log('选中非叶子节点:', selectedKeys[0]);
}
};
const initTreeData = async () => {
let tree: any[] = [];
//
const res = await fetchProdList({});
console.log('res: ', res);
if (res && Array.isArray(res) && res.length) {
tree = res.map((e) => {
return {
title: e.name,
key: e.id,
isLeaf: false,
children: [],
};
});
for (let i = 0; i < tree.length; i++) {
let item = tree[i];
//
const { data } = await fetchVersionPageList({
productId: item.key,
current: 1,
size: 999,
});
item.children = (data as any).map((e) => {
return {
title: e.name,
key: e.uid,
id: e.id,
isLeaf: false,
children: [],
};
});
//
for (let j = 0; j < item.children.length; j++) {
let childItem = item.children[j];
const {
data: { records },
} = await fetchIssuePageList({
versionId: childItem.id,
current: 1,
size: 999,
});
console.log('res: ', res);
childItem.children = (records as any).map((e) => {
return {
title: e.title,
key: e.id,
id: e.id,
isLeaf: true,
};
});
}
}
}
treeData.value = tree;
expandAll.value = true;
console.log('treeData.value: ', treeData.value);
};
initTreeData();
</script>
<style lang="less">
.ql-editor {
height: 400px !important;
}
.knowledge-base {
padding: 24px;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e7eb 100%);
min-height: 100vh;
.tree-card,
.content-card {
min-height: calc(100vh - 50px);
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.quick-actions {
margin-bottom: 16px;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
}
.content-area {
min-height: 300px;
max-height: calc(100vh - 164px);
overflow: scroll;
.selected-content {
padding: 16px;
background: #fafafa;
border-radius: 4px;
}
}
}
}
</style>

165
src/views/client/knowledgeBase/index.vue

@ -0,0 +1,165 @@
<template>
<div class="knowledge-base">
<a-row :gutter="16">
<!-- 左侧目录树 -->
<a-col :span="6">
<a-card class="tree-card">
<template #title>
<span class="card-title"> <folder-outlined /> 知识库目录 </span>
</template>
<a-tree
v-model:selectedKeys="selectedKeys"
v-model:expandedKeys="expandedKeys"
:tree-data="treeData"
@select="onSelect"
/>
</a-card>
</a-col>
<!-- 右侧内容区 -->
<a-col :span="18">
<a-card class="content-card">
<template #title>
<span class="card-title"> <book-outlined /> 知识库内容 </span>
</template>
<!-- 内容展示区域 -->
<div class="content-area">
<template v-if="selectedKeys.length">
<!-- 这里可以根据选中的目录显示具体内容 -->
<div class="selected-content"> 已选择: {{ selectedKeys[0] }} </div>
</template>
<template v-else>
<a-empty description="请选择左侧目录" />
</template>
</div>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import {
FolderOutlined,
BookOutlined,
FileTextOutlined,
TeamOutlined,
SettingOutlined,
PlusOutlined,
EditOutlined,
DeleteOutlined,
ExportOutlined,
} from '@ant-design/icons-vue';
import { DictEnum } from '@/enums/dictEnum';
import { getDictionaryByTypeName } from '@/utils/dict';
//
const treeData = ref([
{
title: '知识库',
key: '1',
children: [],
},
{
title: '问题工单',
key: '2',
children: [],
},
]);
//
const selectedKeys = ref<string[]>([]);
const expandedKeys = ref<string[]>(['1', '2']);
//
const featureCards = ref([
{
title: '文档管理',
description: '集中管理所有知识文档',
icon: FileTextOutlined,
color: '#1890ff',
},
{
title: '团队协作',
description: '支持多人协同编辑',
icon: TeamOutlined,
color: '#52c41a',
},
{
title: '系统设置',
description: '自定义知识库配置',
icon: SettingOutlined,
color: '#722ed1',
},
]);
//
const quickActions = ref([
{
title: '新建文档',
icon: PlusOutlined,
onClick: () => console.log('新建文档'),
},
{
title: '编辑文档',
icon: EditOutlined,
onClick: () => console.log('编辑文档'),
},
{
title: '删除文档',
icon: DeleteOutlined,
onClick: () => console.log('删除文档'),
},
{
title: '导出文档',
icon: ExportOutlined,
onClick: () => console.log('导出文档'),
},
]);
//
const onSelect = (selectedKeys: string[], info: any) => {
console.log('selected', selectedKeys, info);
};
const initTreeData = async () => {
const res = await getDictionaryByTypeName(DictEnum.TAG_TYPE);
console.log('res: ', res);
};
initTreeData();
</script>
<style lang="less" scoped>
.knowledge-base {
padding: 24px;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e7eb 100%);
min-height: 100vh;
.tree-card,
.content-card {
min-height: calc(100vh - 50px);
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.quick-actions {
margin-bottom: 16px;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
}
.content-area {
padding: 16px 0;
min-height: 300px;
.selected-content {
padding: 16px;
background: #fafafa;
border-radius: 4px;
}
}
}
}
</style>

179
src/views/knowledgeBase/index.vue

@ -1,179 +0,0 @@
<template>
<div class="knowledge-base">
<a-row :gutter="16">
<!-- 左侧目录树 -->
<a-col :span="6">
<a-card class="tree-card">
<template #title>
<span class="card-title">
<folder-outlined /> 知识库目录
</span>
</template>
<a-tree
v-model:selectedKeys="selectedKeys"
v-model:expandedKeys="expandedKeys"
:tree-data="treeData"
@select="onSelect"
/>
</a-card>
</a-col>
<!-- 右侧内容区 -->
<a-col :span="18">
<a-card class="content-card">
<template #title>
<span class="card-title">
<book-outlined /> 知识库内容
</span>
</template>
<!-- 快捷操作区域 -->
<div class="quick-actions">
<a-space>
<a-button type="primary" v-for="action in quickActions" :key="action.title" @click="action.onClick">
<template #icon><component :is="action.icon" /></template>
{{ action.title }}
</a-button>
</a-space>
</div>
<!-- 内容展示区域 -->
<div class="content-area">
<template v-if="selectedKeys.length">
<!-- 这里可以根据选中的目录显示具体内容 -->
<div class="selected-content">
已选择: {{ selectedKeys[0] }}
</div>
</template>
<template v-else>
<a-empty description="请选择左侧目录" />
</template>
</div>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import {
FolderOutlined,
BookOutlined,
FileTextOutlined,
TeamOutlined,
SettingOutlined,
PlusOutlined,
EditOutlined,
DeleteOutlined,
ExportOutlined
} from '@ant-design/icons-vue';
//
const treeData = ref([
{
title: '产品文档',
key: '1',
children: [
{ title: '使用指南', key: '1-1' },
{ title: '常见问题', key: '1-2' },
],
},
{
title: '技术文档',
key: '2',
children: [
{ title: 'API文档', key: '2-1' },
{ title: '开发规范', key: '2-2' },
],
},
]);
//
const selectedKeys = ref<string[]>([]);
const expandedKeys = ref<string[]>(['1', '2']);
//
const featureCards = ref([
{
title: '文档管理',
description: '集中管理所有知识文档',
icon: FileTextOutlined,
color: '#1890ff',
},
{
title: '团队协作',
description: '支持多人协同编辑',
icon: TeamOutlined,
color: '#52c41a',
},
{
title: '系统设置',
description: '自定义知识库配置',
icon: SettingOutlined,
color: '#722ed1',
},
]);
//
const quickActions = ref([
{
title: '新建文档',
icon: PlusOutlined,
onClick: () => console.log('新建文档'),
},
{
title: '编辑文档',
icon: EditOutlined,
onClick: () => console.log('编辑文档'),
},
{
title: '删除文档',
icon: DeleteOutlined,
onClick: () => console.log('删除文档'),
},
{
title: '导出文档',
icon: ExportOutlined,
onClick: () => console.log('导出文档'),
},
]);
//
const onSelect = (selectedKeys: string[], info: any) => {
console.log('selected', selectedKeys, info);
};
</script>
<style lang="less" scoped>
.knowledge-base {
padding: 24px;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e7eb 100%);
min-height: 100vh;
.tree-card,
.content-card {
min-height: calc(100vh - 50px);
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.quick-actions {
margin-bottom: 16px;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
}
.content-area {
padding: 16px 0;
min-height: 300px;
.selected-content {
padding: 16px;
background: #fafafa;
border-radius: 4px;
}
}
}
}
</style>

113
src/views/login/index.vue

@ -4,7 +4,7 @@
<div class="login-content">
<div class="login-header">
<div class="logo-wrapper">
<!-- <img src="@/assets/logo.png" alt="logo" class="logo-img" /> -->
<img src="@/assets/icons/logo.png" alt="logo" class="logo-img" />
</div>
<h1 class="login-title">问题工单管理系统</h1>
<div class="title-divider">
@ -26,7 +26,17 @@
<template #prefix><user-outlined /></template>
</a-input>
</a-form-item>
<!-- 邮箱 -->
<a-form-item v-if="state.isRegister">
<a-input
v-model:value="state.formInline.email"
size="large"
placeholder="请输入邮箱"
class="custom-input"
>
<template #prefix><safety-outlined /></template>
</a-input>
</a-form-item>
<a-form-item>
<a-input
v-model:value="state.formInline.password"
@ -67,7 +77,7 @@
</a-form-item>
<div class="form-footer flex justify-center">
<a @click="toggleMode">{{
<a @click="toggleMode" class="text-[#e6400b]">{{
state.isRegister ? '已有账号?去登录' : '没有账号?去注册'
}}</a>
</div>
@ -92,6 +102,7 @@
formInline: {
username: 'admin',
password: '123',
email: '', //
confirmPassword: '', //
},
});
@ -166,21 +177,89 @@
display: flex;
width: 100vw;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: url('@/assets/images/background.jpg') no-repeat center center fixed;
background-size: cover;
// background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
// &::before,
// &::after {
// content: '';
// position: absolute;
// width: 1000px;
// height: 1000px;
// border-radius: 50%;
// background: linear-gradient(
// 135deg,
// rgba(255, 255, 255, 0.1) 0%,
// rgba(255, 255, 255, 0.05) 100%
// );
// animation: float 20s infinite linear;
// }
// &::before {
// top: -400px;
// right: -200px;
// animation-delay: -5s;
// }
// &::after {
// bottom: -400px;
// left: -200px;
// animation-duration: 25s;
// }
}
.login-box {
width: 460px;
padding: 40px;
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
background: rgba(255, 255, 255, 0.4);
border-radius: 24px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(255, 255, 255, 0.1) inset;
backdrop-filter: blur(12px);
animation: fadeIn 0.5s ease-out;
position: relative;
z-index: 1;
&::before {
content: '';
position: absolute;
inset: 0;
border-radius: 24px;
padding: 2px;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.1));
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
}
@keyframes float {
0% {
transform: rotate(0deg) translate(0, 0);
}
50% {
transform: rotate(180deg) translate(100px, 50px);
}
100% {
transform: rotate(360deg) translate(0, 0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.login-content {
.login-header {
text-align: center;
@ -188,9 +267,12 @@
.logo-wrapper {
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: center;
.logo-img {
width: 64px;
width: 150px;
height: 64px;
object-fit: contain;
}
@ -199,7 +281,7 @@
.login-title {
font-size: 32px;
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #e6400b 0%, #ec7049 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 16px;
@ -215,17 +297,17 @@
.line {
width: 40px;
height: 1px;
background: linear-gradient(90deg, transparent, #667eea);
background: linear-gradient(90deg, transparent, #e6400b);
&:last-child {
background: linear-gradient(90deg, #764ba2, transparent);
background: linear-gradient(90deg, #ec7049, transparent);
}
}
.dot {
width: 6px;
height: 6px;
background: #667eea;
background: #e6400b;
border-radius: 50%;
margin: 0 8px;
}
@ -247,7 +329,7 @@
transform: translateX(-50%);
width: 0;
height: 2px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #e6400b 0%, #ec7049 100%);
transition: width 0.3s ease;
}
@ -256,6 +338,7 @@
}
}
}
}
.custom-input {
@ -291,7 +374,7 @@
height: 46px;
font-size: 16px;
border-radius: 8px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #e6400b 0%, #ec7049 100%);
border: none;
transition: all 0.3s;

8
src/views/question/issue/columns.tsx

@ -35,6 +35,13 @@ export const baseColumns: TableColumnItem[] = [
required: false,
},
},
{
title: '标签',
align: 'center',
dataIndex: 'tags',
width: 200,
hideInSearch: true,
},
// {
// title: '问题属性',
// align: 'center',
@ -139,6 +146,7 @@ export const baseColumns: TableColumnItem[] = [
align: 'center',
dataIndex: 'state',
width: 150,
fixed: 'right',
formItemProps: {
defaultValue: undefined,
required: false,

14
src/views/question/issue/data.ts

@ -1,11 +1,11 @@
export const stateTypeList: any = [
// Init(0,"初始化"),
// Init(0,"待处理"),
// Back(1,"退回"),
// Develop(2,"开发"),
// Test(3,"测试"),
// Develop(2,"开发"),
// Test(3,"测试"),
// End(4,"结束"),
{
label: '初始化',
label: '待处理',
value: 0,
color: '#f50',
},
@ -15,17 +15,17 @@ export const stateTypeList: any = [
color: '#2db7f5',
},
{
label: '开发',
label: '开发',
value: 2,
color: '#87d068',
},
{
label: '测试',
label: '测试',
value: 3,
color: '#108ee9',
},
{
label: '结束',
label: '已解决',
value: 4,
color: '#f50',
},

43
src/views/question/issue/detail.vue

@ -13,7 +13,7 @@
<a-spin :spinning="loading">
<div class="my-3">
<a-steps :current="current" size="small">
<a-step title="初始化">
<a-step title="待处理">
<template #icon>
<SvgIcon :size="22" name="init" />
</template>
@ -24,17 +24,17 @@
<SvgIcon :size="24" name="back" />
</template>
</a-step>
<a-step title="开发">
<a-step title="开发">
<template #icon>
<SvgIcon :size="24" name="dev" />
</template>
</a-step>
<a-step title="测试">
<a-step title="测试">
<template #icon>
<SvgIcon :size="24" name="test" />
</template>
</a-step>
<a-step title="结束">
<a-step title="已解决">
<template #icon>
<SvgIcon :size="24" name="end" />
</template>
@ -79,10 +79,18 @@
import { useForm } from '@/components/core/schema-form';
import { getEditFormSchema } from './formSchemas';
import { useRoute } from 'vue-router';
import { onMounted, ref } from 'vue';
import { onMounted, ref, watch } from 'vue';
import { findOneById } from '@/api/issue';
import { SvgIcon } from '@/components/basic/svg-icon';
import { stateTypeList } from './data';
const props = defineProps({
id: {
type: String,
default: '',
},
});
const route = useRoute();
const editorOptions = ref({
theme: 'snow',
@ -107,8 +115,6 @@
const current = ref(0);
const { id } = route.query;
const [SchemaForm, formRef] = useForm({
labelWidth: 150,
schemas: getEditFormSchema({}, true),
@ -122,7 +128,14 @@
disabled: true,
});
onMounted(async () => {
const initData = async () => {
// route.query.idprops.id
const id = props.id || route.query.id;
if (!id) {
return;
}
loading.value = true;
const res = await findOneById({ id: id as string });
loading.value = false;
@ -151,6 +164,20 @@
formRef?.setFieldsValue(res);
formRef?.clearValidate();
};
watch(
() => props.id,
(newVal) => {
if (newVal) {
initData();
}
},
{ immediate: true, deep: true },
);
onMounted(async () => {
initData();
});
</script>
<style lang="less" scoped></style>

20
src/views/question/issue/formSchemas.tsx

@ -39,6 +39,25 @@ export const getEditFormSchema: (
},
rules: [{ required: true }],
},
{
field: 'tags',
component: 'Select',
componentProps: {
request: async () => {
const data = await getDictionaryByTypeName(DictEnum.TAG_TYPE);
return data;
},
multiple: true,
placeholder: '请选择标签',
mode: 'tags',
allowClear: true,
},
label: '标签',
colProps: {
span: 12,
},
rules: [{ required: true, type: 'array' }],
},
// {
// label: '问题属性',
// field: 'arrtibute',
@ -168,6 +187,7 @@ export const getEditFormSchema: (
{
field: 'state',
component: 'Select',
defaultValue: 0,
label: '状态',
colProps: {
span: 12,

47
src/views/question/issue/index.vue

@ -55,6 +55,7 @@
<template #solution="{ formModel, field }">
<div class="ql-box">
<QuillEditor
ref="quillEditor"
theme="snow"
v-model:content="formModel[field]"
:options="editorOptions"
@ -81,7 +82,7 @@
updateIssueState,
addToknowledge,
} from '@/api/issue';
import { computed, nextTick, ref } from 'vue';
import { computed, nextTick, ref, watch } from 'vue';
import { Modal, message, Alert } from 'ant-design-vue';
import { useRouter } from 'vue-router';
import { useFormModal } from '@/hooks/useModal/index';
@ -100,7 +101,7 @@
const curRecord = ref<Partial<TableListItem>>({});
const visible = ref(false);
const quillEditor = ref<InstanceType<typeof QuillEditor> | null>(null);
const editorOptions = ref({
theme: 'snow',
modules: {
@ -130,6 +131,17 @@
},
});
watch(
() => curRecord.value,
(newVal) => {
formRef?.setFieldsValue(newVal);
},
{
immediate: true,
deep: true,
},
);
const handleOk = async () => {
const values = await formRef?.validate();
if (values) {
@ -152,6 +164,10 @@
values.fileIds = values.files.map((e) => e.id).join(',');
}
if (values?.tags && Array.isArray(values.tags) && values.tags.length) {
values.tags = values.tags.join(',');
}
await (values.id ? updateIssue : createIssue)(values);
message.success(`${values.id ? '编辑' : '新增'}成功`);
visible.value = false;
@ -188,7 +204,7 @@
color: '#3b82f6',
size: '15',
label: '修改',
ifShow: stateText !== '结束',
// ifShow: stateText !== '',
onClick: () => handleEdit(record),
},
{
@ -204,7 +220,7 @@
size: '15',
title: '开发',
label: '开发',
ifShow: stateText === '初始化',
ifShow: stateText === '待处理',
onClick: () => changeState(record, 2),
},
{
@ -212,7 +228,7 @@
size: '20',
title: '退回',
label: '退回',
ifShow: stateText === '初始化',
ifShow: stateText === '待处理',
onClick: () => changeState(record, 1),
},
{
@ -220,7 +236,7 @@
size: '15',
title: '测试',
label: '测试',
ifShow: stateText === '开发',
ifShow: stateText === '开发',
onClick: () => changeState(record, 3),
},
{
@ -228,7 +244,7 @@
size: '15',
title: '结束',
label: '结束',
ifShow: stateText === '测试',
ifShow: stateText === '测试',
onClick: () => changeState(record, 4),
},
{
@ -236,7 +252,7 @@
size: '15',
title: '添加到知识库',
label: '添加到知识库',
ifShow: stateText === '结束',
ifShow: stateText === '已解决',
onClick: () => handleAddToKnowledge(record),
},
{
@ -284,6 +300,10 @@
values.fileIds = values.files.map((e) => e.id).join(',');
}
if (values?.tags && Array.isArray(values.tags) && values.tags.length) {
values.tags = values.tags.join(',');
}
await (record.id ? updateIssue : createIssue)(values);
message.success(`${record.id ? '编辑' : '新增'}成功`);
dynamicTableInstance?.reload();
@ -379,7 +399,9 @@
};
const handleAdd = () => {
formRef?.setFieldsValue(curRecord.value);
curRecord.value = {};
// formRef?.setFieldsValue(curRecord.value);
quillEditor.value?.setContents('');
resetFormFields();
openModal();
};
@ -399,9 +421,14 @@
};
});
}
if (res.tags) {
res.tags = res.tags.split(',') || [];
}
curRecord.value = res;
formRef?.setFieldsValue(res);
quillEditor.value?.setContents(res?.solution);
// formRef?.setFieldsValue(res);
if (res?.productId) {
const { data } = await fetchVersionPageList({

8
src/views/question/knowledge/columns.tsx

@ -1,11 +1,7 @@
import type { TableColumn } from '@/components/core/dynamic-table';
import { DictEnum } from '@/enums/dictEnum';
import { getDictionaryByTypeName } from '@/utils/dict';
export type TableListItem = API.KnowledgeBaseType;
export type TableColumnItem = TableColumn<TableListItem>;
// const questionTypeList = await getDictionaryByTypeName(DictEnum.QUESTION_TYPE);
// 数据项类型
// export type ListItemType = typeof tableData[number];
// 使用TableColumn<ListItemType> 将会限制dataIndex的类型,但换来的是dataIndex有类型提示
@ -40,10 +36,6 @@ export const baseColumns: TableColumnItem[] = [
dataIndex: 'tags',
width: 200,
hideInSearch: true,
customRender: ({ record }) => {
const tags = record.tags?.join(', ');
return <div>{{ tags }}</div>;
},
},
{
title: '创建时间',

39
src/views/question/knowledge/formSchemas.tsx

@ -8,14 +8,11 @@ import { stateTypeList } from './data';
import { DictEnum } from '@/enums/dictEnum';
import { getDictionaryByTypeName } from '@/utils/dict';
import { fetchProdList, fetchVersionPageList } from '@/api/prodVersion';
const questionTypeList = await getDictionaryByTypeName(DictEnum.QUESTION_TYPE);
// 编辑页字段
export const getEditFormSchema: (
row?: Partial<TableListItem>,
isDetail?: boolean,
) => FormSchema[] = (record = {}, isDetail = false) => {
console.log('questionTypeList: ', questionTypeList);
return [
{
field: 'title',
@ -24,38 +21,46 @@ export const getEditFormSchema: (
showCount: true,
maxlength: 150,
},
label: '问题标题',
label: '知识库标题',
colProps: {
span: 12,
},
rules: [{ required: true }],
},
{
field: 'versionId',
component: 'Select',
componentProps: {
options: [],
},
label: '版本',
colProps: {
span: 12,
},
},
{
field: 'description',
component: 'InputTextArea',
componentProps: {
rows: 4,
placeholder: '请输入问题描述',
placeholder: '请输入知识库描述',
showCount: true,
maxlength: 150,
},
label: '问题描述',
label: '知识库描述',
colProps: {
span: 24,
},
rules: [{ required: true }],
},
{
field: 'tags',
component: 'Select',
componentProps: {
request: async () => {
const data = await getDictionaryByTypeName(DictEnum.TAG_TYPE);
return data;
},
multiple: true,
placeholder: '请选择标签',
mode: 'tags',
allowClear: true,
},
label: '标签',
colProps: {
span: 12,
},
},
{
label: '解决方案',
field: 'solution',

97
src/views/question/knowledge/index.vue

@ -12,7 +12,7 @@
<DynamicTable
size="small"
showIndex
headerTitle="问题工单列表"
headerTitle="知识库列表"
titleTooltip=""
:data-request="loadData"
:columns="columns"
@ -47,7 +47,7 @@
height: '70vh',
}"
:force-render="true"
:title="`${curRecord.id ? '编辑' : '新增'}问题工单`"
:title="`${curRecord.id ? '编辑' : '新增'}知识库`"
@ok="handleOk"
@cancel="handleCancel"
>
@ -72,15 +72,13 @@
import { type TableListItem, baseColumns } from './columns';
import { useTable, type OnChangeCallbackParams } from '@/components/core/dynamic-table';
import {
fetchIssuePageList,
createIssue,
updateIssue,
deleteIssueById,
deleteBatchIssueById,
fetchKnowledgeBasePageList,
createKnowledgeBase,
updateKnowledgeBase,
deleteKnowledgeBaseById,
deleteBatchKnowledgeBaseById,
findOneById,
updateIssueState,
addToknowledge,
} from '@/api/issue';
} from '@/api/knowledgeBase';
import { computed, nextTick, ref } from 'vue';
import { Modal, message, Alert } from 'ant-design-vue';
import { useRouter } from 'vue-router';
@ -115,8 +113,6 @@
const [showModal] = useFormModal();
const [showFlowModal] = useFormModal();
const [SchemaForm, formRef] = useForm({
labelWidth: 100,
labelAlign: 'right',
@ -152,7 +148,11 @@
values.fileIds = values.files.map((e) => e.id).join(',');
}
await (values.id ? updateIssue : createIssue)(values);
if (values?.tags && Array.isArray(values.tags) && values.tags.length) {
values.tags = values.tags.join(',');
}
await (values.id ? updateKnowledgeBase : createKnowledgeBase)(values);
message.success(`${values.id ? '编辑' : '新增'}成功`);
visible.value = false;
resetFormFields();
@ -204,19 +204,19 @@
},
];
const changeState = async (record: Partial<TableListItem>, state: number) => {
await openFlowModal(record, state);
};
/**
* @description 打开问题工单弹窗
* @description 打开知识库弹窗
*/
const openIssueModal = async (record: Partial<TableListItem> = {}, isReadOnly = false) => {
const openKnowledgeBaseModal = async (
record: Partial<TableListItem> = {},
isReadOnly = false,
) => {
const [formRef] = await showModal<any>({
modalProps: {
title: `${isReadOnly ? '查看' : record.id ? '编辑' : '新增'}问题工单`,
title: `${isReadOnly ? '查看' : record.id ? '编辑' : '新增'}知识库`,
width: 700,
onFinish: async (values) => {
console.log('新增/编辑问题工单', values);
console.log('新增/编辑知识库', values);
values.id = record.id;
if (values.files && Array.isArray(values.files) && values.files.length) {
@ -235,7 +235,7 @@
values.fileIds = values.files.map((e) => e.id).join(',');
}
await (record.id ? updateIssue : createIssue)(values);
await (record.id ? updateKnowledgeBase : createKnowledgeBase)(values);
message.success(`${record.id ? '编辑' : '新增'}成功`);
dynamicTableInstance?.reload();
},
@ -273,32 +273,6 @@
}
};
const openFlowModal = async (record: Partial<TableListItem> = {}, state) => {
const flowTitle = stateTypeList.find((item) => item.value === state)?.label;
const [formRef] = await showFlowModal<any>({
modalProps: {
title: `${flowTitle}问题工单`,
width: 700,
onFinish: async (values) => {
await updateIssueState({
id: record.id,
state: state,
...values,
});
message.success(`${flowTitle}成功`);
dynamicTableInstance?.reload();
},
},
formProps: {
labelWidth: 100,
schemas: getFlowFormSchema(record) as any,
autoSubmitOnEnter: true,
},
});
formRef?.setFieldsValue(record);
};
const handleView = (record: TableListItem) => {
router.push({
path: '/question/issueDetail',
@ -317,18 +291,6 @@
formRef.clearValidate();
};
const handleAddToKnowledge = async (record: TableListItem) => {
console.log('record: ', record);
if (!record.id) return;
const res = await addToknowledge({
id: record.id,
});
if (res) {
message.success('添加到知识库成功');
dynamicTableInstance?.reload();
}
};
const handleAdd = () => {
formRef?.setFieldsValue(curRecord.value);
resetFormFields();
@ -350,6 +312,11 @@
};
});
}
if (res.tags) {
res.tags = res.tags.split(',') || [];
}
curRecord.value = res;
formRef?.setFieldsValue(res);
@ -381,10 +348,6 @@
}
};
const handleShow = async (record: TableListItem) => {
openIssueModal(record, true);
};
const handleDelete = (id: string) => {
delRowConfirm(id);
};
@ -394,15 +357,15 @@
*/
const delRowConfirm = async (id: string | string[]) => {
Modal.confirm({
title: '确定要删除所选的问题工单吗?',
title: '确定要删除所选的知识库吗?',
icon: <ExclamationCircleOutlined />,
centered: true,
onOk: async () => {
if (Array.isArray(id)) {
//
await deleteBatchIssueById(id).finally(dynamicTableInstance?.reload);
await deleteBatchKnowledgeBaseById(id).finally(dynamicTableInstance?.reload);
} else {
await deleteIssueById({ id }).finally(dynamicTableInstance?.reload);
await deleteKnowledgeBaseById({ id }).finally(dynamicTableInstance?.reload);
}
},
});
@ -486,7 +449,7 @@
: undefined,
};
const res = await fetchIssuePageList({
const res = await fetchKnowledgeBasePageList({
...params,
...dateRange,
current: params.page,

1
src/views/system/prodVersion/Tree.vue

@ -60,7 +60,6 @@
import { Nullable } from '@/utils/types';
import { prodFormSchema } from './formSchemas';
import { TableListItem } from './columns';
import { omit } from 'lodash-es';
import { isArray } from '@/utils/is';
export default defineComponent({

11
src/views/system/user/columns.tsx

@ -44,7 +44,16 @@ export const baseColumns: TableColumnItem[] = [
required: false,
},
},
{
title: '邮箱',
align: 'center',
dataIndex: 'email',
width: 150,
formItemProps: {
defaultValue: '',
required: false,
},
},
{
title: '性别',
align: 'center',

18
src/views/system/user/formSchemas.ts

@ -31,12 +31,30 @@ export const userSchemas: FormSchema<API.CreateUserParams>[] = [
{
field: 'password',
component: 'Input',
componentProps: {
type: 'password',
},
label: '密码',
colProps: {
span: 12,
},
rules: [{ required: true }],
},
{
field: 'email',
component: 'Input',
label: '邮箱',
colProps: {
span: 12,
},
rules: [
{
required: true,
message: '请输入正确格式的邮箱',
pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
},
],
},
{
field: 'mobile',
component: 'Input',

7
vite.config.ts

@ -10,6 +10,7 @@ import { terser } from 'rollup-plugin-terser';
import WindiCSS from 'vite-plugin-windicss';
import path from 'path';
import Markdown from 'vite-plugin-md';
import topLevelAwait from 'vite-plugin-top-level-await';
const CWD = process.cwd();
@ -85,6 +86,12 @@ export default defineConfig({
},
}),
} as any,
topLevelAwait({
// The export name of top-level await promise for each chunk module
promiseExportName: '__tla',
// The function to generate import names of top-level await promise in each chunk module
promiseImportName: (i) => `__tla_${i}`,
}),
],
css: {
preprocessorOptions: {

100
yarn.lock

@ -553,6 +553,11 @@
resolved "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@rollup/plugin-virtual@^3.0.2":
version "3.0.2"
resolved "https://registry.npmmirror.com/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz#17e17eeecb4c9fa1c0a6e72c9e5f66382fddbb82"
integrity sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==
"@rollup/pluginutils@^4.2.1":
version "4.2.1"
resolved "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz"
@ -578,6 +583,87 @@
core-js "^3.15.1"
nanopop "^2.1.0"
"@swc/core-darwin-arm64@1.11.18":
version "1.11.18"
resolved "https://registry.npmmirror.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.18.tgz#955ba60baf78d43badf6261f3de077ddec7447b0"
integrity sha512-K6AntdUlNMQg8aChqjeXwnVhK6d4WRZ9TgtLSTmdU0Ugll4an7QK49s9NrT7XQU91cEsVvzdr++p1bNImx0hJg==
"@swc/core-darwin-x64@1.11.18":
version "1.11.18"
resolved "https://registry.npmmirror.com/@swc/core-darwin-x64/-/core-darwin-x64-1.11.18.tgz#273d4407a65840d0d0c02e0970bb2f43ec5f5375"
integrity sha512-RCRvC6Q9M5BArTvj/IzUAAYGrgxYFbTTnAtf6UX7JFq2DAn+hEwYUjmC1m0gFso9HqFU0m5QZUGfZvVmACGWUw==
"@swc/core-linux-arm-gnueabihf@1.11.18":
version "1.11.18"
resolved "https://registry.npmmirror.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.18.tgz#920aedc52ab53b87b564ddd9d1409fa09494f552"
integrity sha512-wteAKf8YKb3jOnZFm3EzuIMzzCVXMuQOLHsz1IgEOc44/gdgNXKxaYTWAowZuej7t68tf/w0cRNMc7Le414v/g==
"@swc/core-linux-arm64-gnu@1.11.18":
version "1.11.18"
resolved "https://registry.npmmirror.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.18.tgz#2683cbe4920464d4fcace286835d5cb7586c50c0"
integrity sha512-hY6jJYZ6PKHSBo5OATswfyKsUgsWu9+4nDcN8liYIRRgz3E0G9wk0VUTP4cFPivBFeHWTTAGz687/Nf2aQEIpw==
"@swc/core-linux-arm64-musl@1.11.18":
version "1.11.18"
resolved "https://registry.npmmirror.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.18.tgz#10b0e29949dfb3c7a29fc2b001ee4ee3ca9f2ead"
integrity sha512-slu0mlP2nucvQalttnapfpqpD/LlM9NHx9g3ofgsLzjObyMEBiX4ZysQ3y65U8Mjw71RNqtLd/ZmvxI6OmLdiQ==
"@swc/core-linux-x64-gnu@1.11.18":
version "1.11.18"
resolved "https://registry.npmmirror.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.18.tgz#a3f66b656a9490e5d437b382a2baad00b02031a5"
integrity sha512-h9a/8PA25arMCQ9t8CE8rA1s0c77z4kCZZ7dUuUkD88yEXIrARMca1IKR7of+S3slfQrf1Zlq3Ac1Fb1HVJziQ==
"@swc/core-linux-x64-musl@1.11.18":
version "1.11.18"
resolved "https://registry.npmmirror.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.18.tgz#a0d0576c4a7ee9329297da30c17537f303437a71"
integrity sha512-0sMDJj5qUGK9QEw4lrxLxkTP/4AoKciqNzXvqbk+J9XuXN2aIv4BsR1Y7z3GwAeMFGsba2lbHLOtJlDsaqIsiA==
"@swc/core-win32-arm64-msvc@1.11.18":
version "1.11.18"
resolved "https://registry.npmmirror.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.18.tgz#dbf21a232143e49efbb0ef0a0354eb6db61da109"
integrity sha512-zGv9HnfgBcKyt54MJRWdwRNu9BuYkAFM7bx+tWtKhd37Ef7ZX20QLs9xXl5wWDXCbsOdRxXIZgXs6PEL+Pzmrw==
"@swc/core-win32-ia32-msvc@1.11.18":
version "1.11.18"
resolved "https://registry.npmmirror.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.18.tgz#818ff1300f3bc0f664531a4d246df11ca475bb24"
integrity sha512-uBKj0S1lYv/E2ZhxHZOxSiQwoegYmzbPRpjq6eHBZDv97mu7W3K27/lsnPbvAfQ6b6rnv8BI+EsmJ7VLQBAHBQ==
"@swc/core-win32-x64-msvc@1.11.18":
version "1.11.18"
resolved "https://registry.npmmirror.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.18.tgz#708483370e5ff08e4441ce037c725fd7af948060"
integrity sha512-8USTRcdgeFMNBgvVXl8tz6n4+9s9m+zHsfDeBT4jPgwnq2bnLBlTUlwnPwzDxfg9nUJr6RFD4xeKfWyZZRosZg==
"@swc/core@^1.10.16":
version "1.11.18"
resolved "https://registry.npmmirror.com/@swc/core/-/core-1.11.18.tgz#8510529d5ac43025573ff7122c99fc254414f41b"
integrity sha512-ORZxyCKKiqYt2iHdh1C7pfVR1GBjkuFOdwqZggQzaq0vt22DpGca+2JsUtkUoWQmWcct04v5+ScwgvsHuMObxA==
dependencies:
"@swc/counter" "^0.1.3"
"@swc/types" "^0.1.21"
optionalDependencies:
"@swc/core-darwin-arm64" "1.11.18"
"@swc/core-darwin-x64" "1.11.18"
"@swc/core-linux-arm-gnueabihf" "1.11.18"
"@swc/core-linux-arm64-gnu" "1.11.18"
"@swc/core-linux-arm64-musl" "1.11.18"
"@swc/core-linux-x64-gnu" "1.11.18"
"@swc/core-linux-x64-musl" "1.11.18"
"@swc/core-win32-arm64-msvc" "1.11.18"
"@swc/core-win32-ia32-msvc" "1.11.18"
"@swc/core-win32-x64-msvc" "1.11.18"
"@swc/counter@^0.1.3":
version "0.1.3"
resolved "https://registry.npmmirror.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9"
integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==
"@swc/types@^0.1.21":
version "0.1.21"
resolved "https://registry.npmmirror.com/@swc/types/-/types-0.1.21.tgz#6fcadbeca1d8bc89e1ab3de4948cef12344a38c0"
integrity sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==
dependencies:
"@swc/counter" "^0.1.3"
"@trysound/sax@0.2.0":
version "0.2.0"
resolved "https://registry.npmmirror.com/@trysound/sax/-/sax-0.2.0.tgz"
@ -4009,6 +4095,11 @@ utils-merge@1.0.1:
resolved "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
uuid@^10.0.0:
version "10.0.0"
resolved "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
vary@^1, vary@~1.1.2:
version "1.1.2"
resolved "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz"
@ -4039,6 +4130,15 @@ vite-plugin-svg-icons@~2.0.1:
svg-baker "1.7.0"
svgo "^2.8.0"
vite-plugin-top-level-await@^1.5.0:
version "1.5.0"
resolved "https://registry.npmmirror.com/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.5.0.tgz#e3f76302921152bf29d1658f169d168f8937e78b"
integrity sha512-r/DtuvHrSqUVk23XpG2cl8gjt1aATMG5cjExXL1BUTcSNab6CzkcPua9BPEc9fuTP5UpwClCxUe3+dNGL0yrgQ==
dependencies:
"@rollup/plugin-virtual" "^3.0.2"
"@swc/core" "^1.10.16"
uuid "^10.0.0"
vite-plugin-windicss@^1.9.0:
version "1.9.0"
resolved "https://registry.npmmirror.com/vite-plugin-windicss/-/vite-plugin-windicss-1.9.0.tgz"

Loading…
Cancel
Save