This commit is contained in:
2024-09-03 17:40:15 +08:00
parent bdb8c55b2b
commit f470a7fece
21 changed files with 383 additions and 71 deletions

View File

@@ -35,7 +35,7 @@
},
"devDependencies": {
"@types/lodash": "^4.14.195",
"@types/node": "^22.5.1",
"@types/node": "^22.5.2",
"@types/nprogress": "^0.2.3",
"@types/qs": "^6.9.15",
"@vitejs/plugin-vue": "^5.1.3",
@@ -45,10 +45,10 @@
"browserslist": "^4.23.0",
"caniuse-lite": "^1.0.30001655",
"eslint": "^9.9.1",
"eslint-plugin-vue": "^9.27.0",
"eslint-plugin-vue": "^9.28.0",
"less": "^4.2.0",
"less-loader": "^12.2.0",
"postcss": "^8.4.42",
"postcss": "^8.4.44",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.10",
"typescript": "^5.5.3",
@@ -1410,9 +1410,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.5.1",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-22.5.1.tgz",
"integrity": "sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==",
"version": "22.5.2",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-22.5.2.tgz",
"integrity": "sha512-acJsPTEqYqulZS/Yp/S3GgeE6GZ0qYODUR8aVr/DkhHQ8l9nd4j5x1/ZJy9/gHrRlFMqkO6i0I3E27Alu4jjPg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2747,9 +2747,9 @@
}
},
"node_modules/eslint-plugin-vue": {
"version": "9.27.0",
"resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-9.27.0.tgz",
"integrity": "sha512-5Dw3yxEyuBSXTzT5/Ge1X5kIkRTQ3nvBn/VwPwInNiZBSJOO/timWMUaflONnFBzU6NhB68lxnCda7ULV5N7LA==",
"version": "9.28.0",
"resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-9.28.0.tgz",
"integrity": "sha512-ShrihdjIhOTxs+MfWun6oJWuk+g/LAhN+CiuOl/jjkG3l0F2AuK5NMTaWqyvBgkFtpYmyks6P4603mLmhNJW8g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2758,7 +2758,7 @@
"natural-compare": "^1.4.0",
"nth-check": "^2.1.1",
"postcss-selector-parser": "^6.0.15",
"semver": "^7.6.0",
"semver": "^7.6.3",
"vue-eslint-parser": "^9.4.3",
"xml-name-validator": "^4.0.0"
},
@@ -2785,10 +2785,11 @@
}
},
"node_modules/eslint-plugin-vue/node_modules/semver": {
"version": "7.6.2",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.6.2.tgz",
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
"version": "7.6.3",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
@@ -4349,9 +4350,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.42",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.42.tgz",
"integrity": "sha512-hywKUQB9Ra4dR1mGhldy5Aj1X3MWDSIA1cEi+Uy0CjheLvP6Ual5RlwMCh8i/X121yEDLDIKBsrCQ8ba3FDMfQ==",
"version": "8.4.44",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.44.tgz",
"integrity": "sha512-Aweb9unOEpQ3ezu4Q00DPvvM2ZTUitJdNKeP/+uQgr1IBIqu574IaZoURId7BKtWMREwzKa9OgzPzezWGPWFQw==",
"funding": [
{
"type": "opencollective",

View File

@@ -38,7 +38,7 @@
},
"devDependencies": {
"@types/lodash": "^4.14.195",
"@types/node": "^22.5.1",
"@types/node": "^22.5.2",
"@types/nprogress": "^0.2.3",
"@types/qs": "^6.9.15",
"@vitejs/plugin-vue": "^5.1.3",
@@ -48,10 +48,10 @@
"browserslist": "^4.23.0",
"caniuse-lite": "^1.0.30001655",
"eslint": "^9.9.1",
"eslint-plugin-vue": "^9.27.0",
"eslint-plugin-vue": "^9.28.0",
"less": "^4.2.0",
"less-loader": "^12.2.0",
"postcss": "^8.4.42",
"postcss": "^8.4.44",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.10",
"typescript": "^5.5.3",

View File

@@ -76,5 +76,17 @@ export default {
method: "get",
params
})
},
/**
* 请求单个case信息
* @params 传入case完整的key
* @params 项目id
*/
getCaseOne(params = {}) {
return request({
url: "/project/getCaseOne",
method: "get",
params
})
}
}

View File

@@ -100,11 +100,7 @@ const openContextMenu = async (ev, record) => {
currentRow.value = record
await nextTick(() => {
const domHeight = document.querySelector(".ma-crud-contextmenu").offsetHeight
if (document.body.offsetHeight - ev.pageY < domHeight) {
top.value = ev.clientY - domHeight
} else {
top.value = ev.clientY
}
top.value = ev.clientY - domHeight
left.value = ev.clientX
})
}
@@ -145,7 +141,7 @@ defineExpose({
background: var(--color-bg-2);
border: 1px solid var(--color-border-2);
padding: 7px 0;
border-radius: 1px;
border-radius: 4px;
li .context-menu-item {
cursor: pointer;
padding: 5px 10px;

View File

@@ -36,6 +36,7 @@ import "tinymce/plugins/searchreplace" // 查找替换
import "tinymce/plugins/table" // 表格
// import "tinymce/plugins/visualblocks" //显示元素范围
import "tinymce/plugins/visualchars" // 显示不可见字符
import "tinymce/plugins/autoresize" // 自动调整高度
import { storeToRefs } from "pinia"
// import "tinymce/plugins/wordcount" // 字数统计
@@ -50,7 +51,7 @@ const props = defineProps({
},
plugins: {
type: [String, Array],
default: "searchreplace visualchars code table nonbreaking lists autosave"
default: "searchreplace visualchars code table nonbreaking lists autosave autoresize"
},
toolbar: {
type: [String, Array],
@@ -142,6 +143,11 @@ const initConfig = reactive({
toolbar: props.toolbar,
skeletonScreen: true,
branding: false,
// autoresize插件和resize配置
resize: true,
min_height: 100,
max_height: 600,
autoresize_bottom_margin: 10,
content_css:
theme.value === "dark"
? "/tinymce/skins/content/dark/content.css"

View File

@@ -1,12 +1,3 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"

View File

@@ -1,12 +1,3 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<ma-form-item
v-if="typeof props.component.display == 'undefined' || props.component.display === true"

View File

@@ -1,5 +1,6 @@
import role from "./role"
// 该文件规定如何role不为对应值则删除dom
// 用法vue的指令 -> v-role="['admin']"即只允许userStore里面role属性为admin才能看见
const checkRole = (el, binding) => {
const { value } = binding

View File

@@ -2,10 +2,7 @@ import { useUserStore } from "@/store"
const role = (name) => {
const userStore = useUserStore()
return (
(userStore.roles && userStore.roles.includes(name)) ||
(userStore.roles && userStore.roles.includes("superAdmin"))
)
return (userStore.role && userStore.role.includes(name)) || (userStore.role && userStore.role.includes("admin"))
}
export default role

View File

@@ -0,0 +1,52 @@
import { ref, onMounted } from "vue"
import { useRoute } from "vue-router"
import caseApi from "@/api/project/case"
import { Message } from "@arco-design/web-vue"
/**
* 用于在组件挂载时根据route.key/project_id获取当前数据页面的case用例信息
*/
export default function () {
// global
const route = useRoute()
// ref
const tempCaseInfo = ref<any>(null)
// 项目id和当前case的key
const { id, key } = route.query
onMounted(async () => {
try {
const res = await caseApi.getCaseOne({ key, projectId: id })
tempCaseInfo.value = res.data
} catch (err) {
Message.error("获取用例信息失败,请检查服务器")
}
})
// hook里面判断函数判断是否该用例未执行或未通过
const caseIsNotPassedOrNotExe = function (): boolean {
if (tempCaseInfo.value) {
const testSteps: any[] = tempCaseInfo.value.testStep
if (testSteps.length > 0) {
return testSteps.some((it) => it.passed === "2")
}
return false
} else {
return false
}
}
return {
tempCaseInfo,
caseIsNotPassedOrNotExe
}
}
// hook外面判断函数
export const caseIsPassed = function (caseInfo: any): boolean {
if (caseInfo) {
const testSteps: any[] = caseInfo.testStep
if (testSteps.length > 0) {
return testSteps.some((it) => it.passed === "2")
}
return false
} else {
return false
}
}

View File

@@ -11,7 +11,7 @@
>
测试管理平台
</a-typography-title>
<a-typography-title :heading="6" class="version">V0.0.2</a-typography-title>
<a-typography-title :heading="6" class="version">V0.0.3</a-typography-title>
<icon-menu-fold
v-if="!topMenu && appStore.device === 'mobile'"
style="font-size: 22px; cursor: pointer"
@@ -30,7 +30,7 @@
<ul class="right-side">
<li>
<a-tooltip content="搜索-暂无">
<a-button class="nav-btn" type="outline" :shape="'circle'">
<a-button class="nav-btn" type="outline" :shape="'circle'" @click="handleTestBtn">
<template #icon>
<icon-search />
</template>
@@ -105,6 +105,15 @@ import { useRouter, useRoute } from "vue-router"
const router = useRouter()
const route = useRoute()
const appStore = useAppStore()
// ~~~测试开始~~~
import { useUserStore } from "@/store"
const userStore = useUserStore()
const handleTestBtn = () => {
console.log(userStore.$state)
}
// ~~~测试结束~~~
// 切换暗黑主题
const handleChangeTheme = () => {
appStore.toggleTheme()

View File

@@ -22,7 +22,7 @@ const DASHBOARD = {
locale: "工作台",
icon: "icon-dashboard",
title: "工作台",
ignoreCache: true,
ignoreCache: true
}
},
{
@@ -43,7 +43,7 @@ const DASHBOARD = {
component: () => import("@/views/dashboard/usermanage/index.vue"),
meta: {
requiresAuth: true,
roles: ["*"],
roles: ["admin"], // 只有管理员admin才看得到
locale: "用户管理",
icon: "icon-user-group",
title: "用户管理"

View File

@@ -9,7 +9,8 @@ const TESTMANAGE = {
icon: "icon-desktop",
order: 1,
locale: "日志监控",
title: "日志监控"
title: "日志监控",
roles: ["admin"] // 只有role=admin的用户才看到该页面
},
children: [
{
@@ -18,7 +19,6 @@ const TESTMANAGE = {
component: () => import("@/views/monitor/operationLog/index.vue"),
meta: {
requiresAuth: true,
roles: ["*"],
locale: "数据操作日志",
icon: "icon-file",
title: "数据操作日志"

View File

@@ -132,7 +132,7 @@ const handleSubmit = async ({ values, errors }) => {
...otherQuery // 将退出时的查询参数放入,这样就不会错误
}
})
// 暂时加载LDAP数据
// 加载LDAP数据/内网/暂定
await userApi.loadLDAPUsers()
} else {
return

View File

@@ -0,0 +1,155 @@
<template>
<a-modal
v-model:visible="visible"
width="80%"
draggable
:okLoading="okLoading"
:title="form.name ? form.name : '请填写用例名称'"
:on-before-ok="handleOkBefore"
>
<a-spin :loading="loading" class="w-full h-full">
<ma-form :columns="columnsOptions" v-model="form" :options="options" ref="maFormRef"></ma-form>
</a-spin>
</a-modal>
</template>
<script setup lang="ts">
import { reactive, ref } from "vue"
import { type ICaseFormInData } from "../types"
import caseApi from "@/api/project/case"
import { Message } from "@arco-design/web-vue"
const visible = ref(false) // 显示和隐藏
const loading = ref(false) // 整个modal的加载状态-根据请求case而定
const okLoading = ref(false) // 提交按钮的loading状态
const form = ref<any>({}) // 表单数据
const maFormRef = ref<any>(null)
// 定义事件
const emit = defineEmits(["caseUpdate"])
// event点击modal的确定修改按钮
const handleOkBefore = async () => {
// 验证表单
const validateRes = await maFormRef.value.validateForm()
if (validateRes) {
// 验证不通过
return false
} else {
// 验证通过
okLoading.value = true
try {
await caseApi.update(form.value.id, form.value)
okLoading.value = false
Message.success("修改用例成功")
emit("caseUpdate", form.value)
return true
} catch (err) {
okLoading.value = false
return false
}
}
}
// 暴露组件方法
const open = async (formData: ICaseFormInData): Promise<void> => {
visible.value = true
loading.value = true
const res = await caseApi.getCaseList({ id: formData.id })
form.value = res.data.items[0]
loading.value = false
}
// options
const options = ref({
showButtons: false
})
// columns
const columnsOptions = reactive([
{
formType: "grid",
cols: [
{ span: 12, formList: [{ title: "用例标识", dataIndex: "ident", disabled: true }] },
{
span: 12,
formList: [{ title: "用例名称", dataIndex: "name", rules: [{ required: true, message: "名称是必填" }] }]
}
]
},
{
formType: "card",
customClass: ["ml-5", "mb-3", "py-0", "px-0"],
title: "人员信息",
formList: [
{
formType: "grid",
cols: [
{ span: 8, formList: [{ title: "设计人员", dataIndex: "designPerson" }] },
{ span: 8, formList: [{ title: "执行人员", dataIndex: "testPerson" }] },
{ span: 8, formList: [{ title: "审核人员", dataIndex: "monitorPerson" }] }
]
}
]
},
{
formType: "grid",
cols: [{ span: 24, formList: [{ title: "用例综述", dataIndex: "summarize" }] }]
},
{
formType: "grid",
cols: [{ span: 24, formList: [{ title: "用例初始化", dataIndex: "initialization" }] }]
},
{
formType: "grid",
cols: [
{ span: 12, formList: [{ title: "前提与约束", dataIndex: "premise" }] },
{ span: 12, formList: [{ title: "执行时间", dataIndex: "exe_time", formType: "date" }] }
]
},
{
title: "测试步骤",
dataIndex: "testStep",
formType: "children-form",
formList: [
{
title: "操作",
dataIndex: "operation",
formType: "editor",
height: 180
},
{
title: "预期",
placeholder: "请输入预期结果",
dataIndex: "expect"
},
{
title: "结果",
dataIndex: "result",
formType: "editor",
height: 180
},
{
title: "是否通过",
dataIndex: "passed",
formType: "radio",
dict: { name: "passType", props: { label: "title", value: "key" } },
rules: [{ required: true, message: "是否通过必填" }]
},
{
title: "执行状态",
dataIndex: "status",
formType: "radio",
dict: { name: "execType", props: { label: "title", value: "key" } },
rules: [{ required: true, message: "执行状态必填" }]
}
]
}
])
defineExpose({
open
})
</script>
<style scoped></style>

View File

@@ -5,25 +5,38 @@
<div>
<a-list>
<a-list-item v-for="(item, index) in transformData" :key="index">
<a-descriptions :data="item.showData" :title="'用例名称:' + item.case" bordered :column="1" />
<div class="text-base mb-2 flex items-center">
<div class="flex-auto">用例名称{{ item.case }}</div>
<a-space>
<div>
<a-button type="primary" shape="round" @click="handleEditClick(item)"
>修改用例</a-button
>
</div>
</a-space>
</div>
<a-descriptions :data="item.showData" bordered :column="1" />
</a-list-item>
</a-list>
</div>
<case-form ref="caseFormRef" @caseUpdate="handleCaseUpdate"></case-form>
</a-modal>
</div>
</template>
<script setup>
<script setup lang="ts">
import { ref, computed } from "vue"
import CaseForm from "./CaseForm/index.vue"
import { type IRelatedCaseItem } from "./types"
const visible = ref(false)
// 数据储存在这里
const data = ref([])
const data = ref<IRelatedCaseItem[]>([])
// 转换为描述数据
const transformData = computed(() => {
return data.value.map((item) => {
// 数组的每一项都要转为{ case:'xxx',showData:[{label:'xxx',value:'xxx2'}] }
const showData = []
const showData: { label: any; value: any }[] = []
for (let key in item) {
let showKey = key
if (key === "case") continue
@@ -42,18 +55,43 @@ const transformData = computed(() => {
if (key === "demand_ident") {
showKey = "测试项标识"
}
showData.push({
label: showKey,
value: item[key]
})
if (key !== "id") {
showData.push({
label: showKey,
value: item[key]
})
}
}
return {
id: item.id,
case: item.case,
showData
}
})
})
function open(caseList) {
// caseForm相关方法
const caseFormRef = ref<InstanceType<typeof CaseForm> | null>(null)
const handleEditClick = (item: any): void => {
caseFormRef.value!.open(item)
}
// 处理caseForm子组件的case信息更变事件
const handleCaseUpdate = (successFormData: any) => {
// 更新列表林的数据
data.value = data.value.map((it) => {
if (it.id === successFormData.id) {
return {
...it,
case: successFormData.name
}
}
return it
})
}
// 暴露方法
function open(caseList: IRelatedCaseItem[]) {
visible.value = true
data.value = caseList
}

View File

@@ -40,10 +40,11 @@
import { ref } from "vue"
import problemApi from "@/api/project/problem"
import problemSingleApi from "@/api/project/singleProblem"
import { Notification } from "@arco-design/web-vue"
import { Message, Notification } from "@arco-design/web-vue"
import { useRoute } from "vue-router"
import CaseModal from "./CaseModal.vue"
import useTreeStore from "@/store/project/treeData"
import { caseIsPassed } from "@/hooks/workarea/currentCasePage"
const route = useRoute()
const treeStore = useTreeStore()
// 定义props
@@ -63,9 +64,19 @@ const emits = defineEmits(["deleted", "relatedOrunrelated"])
// ~~~定义关联的switch-值改变处理~~~ 该函数返回false或返回Promise[reject]则停止切换
/// 定义个switch的加载loading属性
const loading = ref(false)
/// 储存打开时赋值的caseInfo
const caseInfo = ref(null)
const handleRelatedChange = async (record) => {
// 因为switch绑定了record.related所以可以动态改变
loading.value = true
// 判断该用例是否是未通过,如果未执行或已通过则不允许关联问题单
if (!caseIsPassed(caseInfo.value)) {
Message.error("该用例没有缓存或无未通过步骤,请切换页面或设置未通过步骤后添加问题单!")
loading.value = false
record.related = !record.related
crudRef.value.refresh()
return false
}
const res = await problemApi
.relateProblem({
case_key: route.query.key,
@@ -107,6 +118,8 @@ const open = (row) => {
if (props.hasRelated === "relatedProblem") {
crudRef.value.requestData() // 手动请求数据
visible.value = true
// 打开时赋值caseInfo
caseInfo.value = row
}
}
// crudOptions设置

View File

@@ -0,0 +1,24 @@
/**
* 接口从后端请求关联的case信息类型
*/
export interface IRelatedCaseItem {
id: number
case: string
demand: string
demand_ident: string
dut: string
round: string
}
interface IDescription {
label: string
value: string
}
/**
* CaseForm传过来数据类型
*/
export interface ICaseFormInData {
id: number
case: string
showData: IDescription[]
}

View File

@@ -32,8 +32,12 @@ import problemApi from "@/api/project/problem"
import { useTreeDataStore } from "@/store"
import ProblemChoose from "./components/ProblemChoose.vue"
import { Message } from "@arco-design/web-vue"
import getCaseInfoHook from "@/hooks/workarea/currentCasePage"
const treeDataStore = useTreeDataStore()
const route = useRoute()
// hook-获取当前用例信息
const { tempCaseInfo, caseIsNotPassedOrNotExe } = getCaseInfoHook()
// const router = useRouter()
const roundNumber = route.query.key.split("-")[0]
const dutNumber = route.query.key.split("-")[1]
@@ -46,7 +50,7 @@ const problemchoose = ref()
// ~~~~关联问题单逻辑~~~~
//// 点击关联按钮
const handleRelatedProblem = () => {
problemchoose.value.open()
problemchoose.value.open(tempCaseInfo.value)
}
//// 当关联a-modal删除一个问题单时通知我刷新表格
const related_reload = () => {
@@ -62,7 +66,12 @@ const crudOptions = ref({
// 列表选项卡配置
tabs: {},
beforeOpenAdd: function () {
// 先判断是否已经有个问题单了,如果有则不让用户创建
// 0.判断当前用例的是否为未通过/未执行
if (!caseIsNotPassedOrNotExe()) {
Message.error("该用例没有缓存或无未通过步骤,请切换页面或设置未通过步骤后添加问题单!")
return false
}
// 1.先判断是否已经有个问题单了,如果有则不让用户创建问题单
if (crudRef.value.getTableData().length >= 1) {
Message.error("该用例已经存在问题单了,可在轮次树节点右键添加无关联问题单")
return false
@@ -389,7 +398,7 @@ const crudColumns = ref([
title: "开发人员",
hide: true,
dataIndex: "designerPerson",
formType: "input",
formType: "input"
},
{
title: "开发方日期",

View File

@@ -156,7 +156,7 @@ const useCrudInit = function () {
search: true,
commonRules: [
{ required: true, message: "标识是必填" },
{ validator: validateBlank, message: "标识格式不正确" },
{ validator: validateBlank, message: "标识格式不正确" }
// { validator: validateWindowFileNameInput }
],
validateTrigger: "blur"
@@ -243,7 +243,7 @@ const useCrudInit = function () {
title: "软件类型",
dataIndex: "soft_type",
hide: true,
search: true,
search: false,
formType: "select",
dict: {
data: [
@@ -477,6 +477,21 @@ const useCrudInit = function () {
props: { label: "title", value: "key" },
tagColors: { 1: "green", 2: "blue", 3: "red", 4: "yellow" }
}
},
{
title: "密级",
align: "center",
dataIndex: "secret",
search: true,
hide: true,
formType: "radio",
addDefaultValue: "1",
addDisabled: true,
dict: {
name: "secret",
translation: true,
props: { label: "title", value: "key" }
}
}
])

View File

@@ -110,12 +110,14 @@
<script lang="jsx" setup>
import { ref } from "vue"
import { useRouter } from "vue-router"
import preview from "./cpns/preview.vue"
import Progress from "./cpns/progress.vue"
import useEnterWorkPlant from "./hooks/useEnterWorkPlant"
import useSeitaiModal from "./hooks/useSeitaiModal"
import useGenerateSecond from "./hooks/useGenerateSecond"
import useCrudInit from "./hooks/useCrudInit"
const router = useRouter()
// crud配置和字段信息定义
const { crudRef, crudOptions, crudColumns } = useCrudInit()
// 点击进入工作区函数 - 每次点击后都清除localStorage中树状目录数据