This commit is contained in:
zhangjianjun 2026-03-16 10:35:51 +08:00
commit 7acef4c569
60 changed files with 4358 additions and 0 deletions

34
.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
# Logs
logs
.vscode
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
package-lock.json
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
/service/dev
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
yarn.lock
*.tsbuildinfo
*.env
src
sub
rbBuildAll.bat

15
Dockerfile Normal file
View File

@ -0,0 +1,15 @@
FROM registry.cn-shanghai.aliyuncs.com/devcon/node:18
RUN git clone https://wujiefeng:sange666@git.u8t.cn/batiao/cy-admin.git
RUN mv cy-admin /app
ADD . /app/src
WORKDIR /app
RUN mkdir /app/log
RUN cd /app && \
chmod +x start.sh && \
npm config set registry https://registry.npmmirror.com/ && \
npm run installModels && \
npm run build
EXPOSE 9290
CMD [ "/app/start.sh" ]

36
api/admin/list.ts Normal file
View File

@ -0,0 +1,36 @@
import Request from "lib/utils/requests";
import roleApi from './role';
export default {
getDataList(params: any) {
return Request({
url: 'yt/user',
method: 'get',
params
})
},
updateData(params: any) {
return Request({
url: 'yt/user',
method: 'put',
data: params
})
},
deleteData(params: any) {
return Request({
url: 'yt/user',
method: 'delete',
params: params
})
},
addData(params: any) {
return Request({
url: 'yt/user',
method: 'post',
data: params
})
},
getRoleList() {
return roleApi.getDataList()
}
}

35
api/admin/role.ts Normal file
View File

@ -0,0 +1,35 @@
import Request from "lib/utils/requests";
import sourceApi from '../config/source';
export default {
getDataList() {
return Request({
url: 'yt/role',
method: 'get',
params: {}
})
},
updateData(params: any) {
return Request({
url: 'yt/role',
method: 'put',
data: params
})
},
deleteData(params: any) {
return Request({
url: 'yt/role',
method: 'delete',
params: params
})
},
addData(params: any) {
return Request({
url: 'yt/role',
method: 'post',
data: params
})
},
getResources: sourceApi.getDataList,
}

17
api/app.ts Normal file
View File

@ -0,0 +1,17 @@
import Request from "lib/utils/requests";
export default {
upload(data: any) {
return Request({
url: "/admin/cmp_upload",
method: "post",
data,
params: {
expire: 24 * 30 * 12 * 10,
},
headers: {
"Content-Type": "multipart/form-data",
},
}).then(res => ({ data: {url: res.data} }))
},
};

63
api/common.ts Normal file
View File

@ -0,0 +1,63 @@
import Request from "lib/utils/requests";
// 翻译
export const translateApi = {
translate(params: { text: string, from?: string, to?: string }) {
return Request({
url: "yt/translate",
method: "get",
params,
})
},
}
// 分类
export const categoryApi = {
getDataList(params: any) {
return Request({
url: 'yt/category',
method: 'get',
params
})
},
updateData(params: any) {
return Request({
url: 'yt/category',
method: 'put',
data: params
})
},
deleteData(params: any) {
return Request({
url: 'yt/category',
method: 'delete',
params: params
})
},
addData(params: any) {
return Request({
url: 'yt/category',
method: 'post',
data: params
})
},
}
// 上传
export const uploadApi = {
upload(params: any) {
return Request({
url: 'yt/upload',
method: 'post',
data: params,
params: {
expire: 24 * 30 * 12 * 10,
},
headers: {
"Content-Type": "multipart/form-data",
},
})
},
}

32
api/config/list.ts Normal file
View File

@ -0,0 +1,32 @@
import Request from "lib/utils/requests";
export default {
getDataList(params: any) {
return Request({
url: 'yt/page',
method: 'get',
params
})
},
updateData(params: any) {
return Request({
url: 'yt/page',
method: 'put',
data: params
})
},
deleteData(params: any) {
return Request({
url: 'yt/page',
method: 'delete',
params: params
})
},
addData(params: any) {
return Request({
url: 'yt/page',
method: 'post',
data: params
})
},
}

32
api/config/source.ts Normal file
View File

@ -0,0 +1,32 @@
import Request from "lib/utils/requests";
export default {
getDataList(params: any) {
return Request({
url: 'yt/resource',
method: 'get',
params
})
},
updateData(params: any) {
return Request({
url: 'yt/resource',
method: 'put',
data: params
})
},
deleteData(params: any) {
return Request({
url: 'yt/resource',
method: 'delete',
params: params
})
},
addData(params: any) {
return Request({
url: 'yt/resource',
method: 'post',
data: params
})
},
}

42
api/document/list.ts Normal file
View File

@ -0,0 +1,42 @@
import Request from "lib/utils/requests";
import { categoryApi, translateApi, uploadApi } from '../common';
export default {
getDataList(params: any) {
return Request({
url: 'yt/doc',
method: 'get',
params
})
},
updateData(params: any) {
return Request({
url: 'yt/doc',
method: 'put',
data: params
})
},
deleteData(params: any) {
return Request({
url: 'yt/doc',
method: 'delete',
params: params
})
},
addData(params: any) {
return Request({
url: 'yt/doc',
method: 'post',
data: params
})
},
getDetail(id: number | string) {
return Request({
url: `yt/doc/${id}`,
method: 'get',
})
},
getCategoryList: categoryApi.getDataList,
...translateApi,
...uploadApi,
}

View File

@ -0,0 +1,2 @@
import { categoryApi } from "../common";
export default categoryApi

42
api/history/list.ts Normal file
View File

@ -0,0 +1,42 @@
import Request from "lib/utils/requests";
import { categoryApi, translateApi } from '../common';
export default {
getDataList(params: any) {
return Request({
url: 'yt/process',
method: 'get',
params
})
},
updateData(params: any) {
return Request({
url: 'yt/process',
method: 'put',
data: params
})
},
deleteData(params: any) {
return Request({
url: 'yt/process',
method: 'delete',
params: params
})
},
addData(params: any) {
return Request({
url: 'yt/process',
method: 'post',
data: params
})
},
getDetail(id: number | string) {
return Request({
url: `yt/process/${id}`,
method: 'get',
})
},
getCategoryList: categoryApi.getDataList,
...translateApi
}

View File

@ -0,0 +1,2 @@
import { categoryApi } from "../common";
export default categoryApi

41
api/jobs/list.ts Normal file
View File

@ -0,0 +1,41 @@
import Request from "lib/utils/requests";
import { translateApi, categoryApi } from '../common';
export default {
getDataList(params: any) {
return Request({
url: 'yt/job',
method: 'get',
params
})
},
updateData(params: any) {
return Request({
url: 'yt/job',
method: 'put',
data: params
})
},
deleteData(params: any) {
return Request({
url: 'yt/job',
method: 'delete',
params: params
})
},
addData(params: any) {
return Request({
url: 'yt/job',
method: 'post',
data: params
})
},
getDetail(id: number | string) {
return Request({
url: `yt/job/${id}`,
method: 'get',
})
},
getCategoryList: categoryApi.getDataList,
...translateApi
}

2
api/jobs/typesManage.ts Normal file
View File

@ -0,0 +1,2 @@
import { categoryApi } from "../common";
export default categoryApi

41
api/login.ts Normal file
View File

@ -0,0 +1,41 @@
import Request from "lib/utils/requests";
export default {
login(data: any) {
return Request({
url: 'yt/user/login',
method: 'post',
data: {
account: data.username,
password: data.password
},
})
},
resetPassWord(data: any) {
return Request({
url: "/admin/v2",
method: "put",
data
})
},
editorConfig(data: any) {
return Request({
url: "/corp/config",
method: "put",
data
})
},
getConfig() {
return Request({
url: "/corp/config",
method: "get"
})
},
flashRole(params: any) {
return Request({
url: "/corp/flash/role",
method: "get",
params
})
},
}

43
api/news/list.ts Normal file
View File

@ -0,0 +1,43 @@
import Request from "lib/utils/requests";
import { translateApi, categoryApi, uploadApi } from '../common';
export default {
getDataList(params: any) {
return Request({
url: 'yt/news',
method: 'get',
params
})
},
updateData(params: any) {
return Request({
url: 'yt/news',
method: 'put',
data: params
})
},
deleteData(params: any) {
return Request({
url: 'yt/news',
method: 'delete',
params: params
})
},
addData(params: any) {
return Request({
url: 'yt/news',
method: 'post',
data: params
})
},
getDetail(id: number | string) {
return Request({
url: `yt/news/${id}`,
method: 'get',
})
},
getCategoryList: categoryApi.getDataList,
...translateApi,
...uploadApi,
}

2
api/news/typesManage.ts Normal file
View File

@ -0,0 +1,2 @@
import { categoryApi } from "../common";
export default categoryApi

5
beforeMount.ts Normal file
View File

@ -0,0 +1,5 @@
import type { App } from 'vue'
export default function BeforeMount(app:App<Element>){
}

View File

@ -0,0 +1,544 @@
<template>
<el-dialog :model-value="show" width="800" :fullscreen="isFullscreen" :close-on-click-modal="false" @close="close"
:show-close="false">
<template #header>
<div class="dialog-header">
<span class="dialog-title">{{ title }}</span>
<div class="dialog-header-actions">
<el-button link circle @click="toggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏'">
<el-icon>
<CopyDocument v-if="isFullscreen" />
<FullScreen v-else />
</el-icon>
</el-button>
<el-button link circle @click="close" title="关闭">
<el-icon size="18">
<Close />
</el-icon>
</el-button>
</div>
</div>
</template>
<el-form v-if="show" ref="formRef" :model="formData" :rules="formRules" label-position="left"
v-loading="detailLoading">
<div v-if="normalFields.length" class="in18-form-top">
<el-form-item v-for="item in normalFields" :key="item.key" :prop="item.key">
<template #label>
{{ item.name }}
</template>
<el-input v-if="item.type === 'input'" v-model="formData[item.key]"
:placeholder="`请输入${item.name}`" />
<el-input v-else-if="item.type === 'textarea'" v-model="formData[item.key]" type="textarea"
:rows="4" :placeholder="`请输入${item.name}`" />
<el-select v-else-if="item.type === 'select'" v-model="formData[item.key]"
:placeholder="`请选择${item.name}`" clearable style="width: 100%">
<el-option v-for="opt in getSelectItems(item)" :key="opt.key" :label="opt.name"
:value="opt.key" />
</el-select>
<div v-else-if="isUploadMediaType(item.type)" style="width:100%;">
<div class="upload-group">
<div class="upload-label">图片</div>
<UploadInput :model-value="(formData[item.key]?.image || []).join(',')"
:type="item.type.includes('images') ? 'images' : 'image'"
:uploadFile="uploadFun" @update:modelValue="(val: string) => {
if (!formData[item.key]) formData[item.key] = { image: [], video: [] };
formData[item.key].image = val ? val.split(',').filter(Boolean) : [];
}" />
</div>
<div class="upload-group">
<div class="upload-label">视频</div>
<UploadInput :model-value="(formData[item.key]?.video || []).join(',')" type="video"
:uploadFile="uploadFun" @update:modelValue="(val: string) => {
if (!formData[item.key]) formData[item.key] = { image: [], video: [] };
formData[item.key].video = val ? val.split(',').filter(Boolean) : [];
}" />
</div>
</div>
<div v-else-if="item.type === 'upload:file'" style="width:100%;" class="upload-file">
<UploadInput :model-value="formData[item.key]" type="file" :accept="item.accept || '*'"
:uploadFile="uploadFun"
@update:modelValue="(val: string) => { formData[item.key] = val }" />
</div>
</el-form-item>
</div>
<div class="in18-form-header">
<div class="in18-form-header-left">
<span>{{ primaryLocale?.name }}</span>
</div>
<div class="in18-form-header-right">
<span v-for="locale in secondaryLocales" :key="locale.key" class="locale-tab"
:class="{ active: activeSecondaryLocale?.key === locale.key }"
@click="activeSecondaryLocale = locale">
{{ locale.name }}
</span>
</div>
</div>
<div v-if="translateFields.length && secondaryLocales.length" class="in18-form-wrapper">
<div class="in18-form-left">
<el-form-item v-for="item in translateFields" :key="item.key"
:prop="`translations.${primaryLocaleKey}.${item.key}`">
<template #label>
{{ item.name }}
</template>
<el-input v-if="item.type === 'input'"
v-model="formData.translations[primaryLocaleKey][item.key]"
:placeholder="`请输入${item.name}`" />
<el-input v-else-if="item.type === 'textarea'"
v-model="formData.translations[primaryLocaleKey][item.key]" type="textarea"
:rows="4" :placeholder="`请输入${item.name}`" />
<el-select v-else-if="item.type === 'select'"
v-model="formData.translations[primaryLocaleKey][item.key]"
:placeholder="`请选择${item.name}`" clearable style="width: 100%">
<el-option v-for="opt in getSelectItems(item)" :key="opt.key" :label="opt.name"
:value="opt.key" />
</el-select>
</el-form-item>
</div>
<div class="in18-form-center">
<el-button v-if="activeSecondaryLocale" type="primary" :loading="translating"
@click="handleTranslate">
翻译
</el-button>
</div>
<div v-if="activeSecondaryLocale" class="in18-form-right">
<el-form-item v-for="item in translateFields" :key="item.key">
<template #label>
{{ item.name }}
</template>
<div v-if="item.type === 'input' || item.type === 'textarea'" class="field-with-translate">
<el-input
v-model="formData.translations[activeSecondaryLocale?.key || ''][item.key]"
:type="item.type === 'textarea' ? 'textarea' : undefined"
:rows="item.type === 'textarea' ? 4 : undefined"
:placeholder="`请输入${item.name}`" />
<el-button link size="small" class="translate-icon"
:loading="translatingFieldKey === item.key" @click="handleTranslateField(item.key)"
title="翻译该字段">
<el-icon color="#409eff">
<Promotion />
</el-icon>
</el-button>
</div>
<el-select v-else-if="item.type === 'select'"
v-model="formData.translations[activeSecondaryLocale?.key || ''][item.key]"
:placeholder="`请选择${item.name}`" clearable style="width: 100%">
<el-option v-for="opt in getSelectItems(item)" :key="opt.key" :label="opt.name"
:value="opt.key" />
</el-select>
</el-form-item>
</div>
</div>
</el-form>
<template #footer>
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="submit">确认提交</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive, computed, watch, ref, nextTick } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
import { Close, Promotion, FullScreen, CopyDocument } from '@element-plus/icons-vue'
import UploadInput from "lib/components/UploadInput.vue";
type FormItemType =
| 'input'
| 'select'
| 'textarea'
| 'upload:images,video'
| 'upload:image,video'
| 'upload:file'
type FormItem = {
name: string
key: string
type: FormItemType
value: string | number | { image: string[]; video: string[] }
items?: { key: string; name: string }[]
getItems?: () => { key: string; name: string }[]
must?: boolean
shouldTranslate?: boolean
accept?: string
[prop: string]: any
}
type LocaleItem = {
name: string
key: string
isPrimary?: boolean
}
const props = withDefaults(
defineProps<{
show: boolean
id: string | number
title?: string
form: FormItem[]
locales: LocaleItem[]
translateApi: (data: { text: string; from?: string; to?: string }) => Promise<any>
detailApi: (id: number) => Promise<any>
uploadFun?: (formData: FormData) => Promise<any>
}>(),
{
show: false,
id: '',
title: '详情',
form: () => [],
locales: () => [],
translateApi: () => Promise.reject({ code: 500, message: 'translateApi not implemented' }),
detailApi: () => Promise.reject({ code: 500, message: 'detailApi not implemented' }),
uploadFun: () => Promise.reject({ code: 500, message: 'uploadFun not implemented' }),
}
)
const emit = defineEmits(['close', 'submit'])
const primaryLocale = computed(() => props.locales.find((l) => l.isPrimary) || props.locales[0])
const primaryLocaleKey = computed(() => primaryLocale.value?.key || 'ZH')
const secondaryLocales = computed(() => props.locales.filter((l) => !l.isPrimary))
const activeSecondaryLocale = ref<LocaleItem | null>(null)
const normalFields = computed(() => props.form.filter((f) => !f.shouldTranslate))
const translateFields = computed(() => props.form.filter((f) => f.shouldTranslate))
function isUploadMediaType(type: string): boolean {
return type === 'upload:images,video' || type === 'upload:image,video'
}
function getSelectItems(item: FormItem): { key: string; name: string }[] {
return item.items ?? item.getItems?.() ?? []
}
function parseTranslateResult(res: any): string {
return res?.data?.text ?? res?.data ?? res?.result ?? (typeof res === 'string' ? res : '')
}
const formRef = ref<FormInstance>()
const formRules = computed<FormRules>(() => {
const rules: FormRules = {}
normalFields.value.forEach((item) => {
if (item.must) {
rules[item.key] = [{ required: true, message: `请输入${item.name}`, trigger: ['blur', 'change'] }]
}
})
translateFields.value.forEach((item) => {
if (item.must) {
const prop = `translations.${primaryLocaleKey.value}.${item.key}`
rules[prop] = [{ required: true, message: `请输入${item.name}`, trigger: ['blur', 'change'] }]
}
})
return rules
})
const formData = reactive<{
[key: string]: any
translations: Record<string, Record<string, any>>
}>({
translations: {}
})
const translating = ref(false)
const translatingFieldKey = ref<string>('')
const isFullscreen = ref(false)
const detailLoading = ref(false)
function toggleFullscreen() {
isFullscreen.value = !isFullscreen.value
}
function resetFormData() {
Object.keys(formData).forEach((k) => {
if (k !== 'translations') delete formData[k]
})
formData.translations = {}
}
function initFormData() {
const normal: Record<string, any> = {}
const translations: Record<string, Record<string, any>> = {}
props.form.forEach((item) => {
if (item.shouldTranslate) {
props.locales.forEach((locale) => {
if (!translations[locale.key]) translations[locale.key] = {}
translations[locale.key][item.key] = item.value ?? ''
})
} else if (isUploadMediaType(item.type)) {
const val: any = item.value
normal[item.key] = (val && typeof val === 'object' && Array.isArray(val.image) && Array.isArray(val.video))
? val
: { image: [], video: [] }
} else {
normal[item.key] = item.value ?? ''
}
})
Object.keys(formData).forEach((k) => {
if (k !== 'translations') delete formData[k]
})
Object.keys(normal).forEach((k) => {
formData[k] = normal[k]
})
formData.translations = translations
if (secondaryLocales.value.length > 0 && !activeSecondaryLocale.value) {
activeSecondaryLocale.value = secondaryLocales.value[0]
} else if (secondaryLocales.value.length > 0) {
const stillExists = secondaryLocales.value.some((l) => l.key === activeSecondaryLocale.value?.key)
if (!stillExists) {
activeSecondaryLocale.value = secondaryLocales.value[0]
}
} else {
activeSecondaryLocale.value = null
}
nextTick(() => formRef.value?.clearValidate())
}
watch(
() => [props.show, props.form, props.locales],
async () => {
if (props.show) {
initFormData()
if (props.id) {
detailLoading.value = true
try {
const data = await props.detailApi(Number(props.id))
if (data) {
const pKey = primaryLocaleKey.value
Object.keys(data).forEach((localeKey) => {
const localeData = data[localeKey]
if (!localeData || typeof localeData !== 'object' || !props.locales.some((l) => l.key === localeKey)) return
Object.keys(localeData).forEach((fieldKey) => {
const field = props.form.find((f) => f.key === fieldKey)
if (!field) return
if (field.shouldTranslate) {
if (!formData.translations[localeKey]) formData.translations[localeKey] = {}
formData.translations[localeKey][fieldKey] = localeData[fieldKey]
} else if (localeKey === pKey) {
formData[fieldKey] = localeData[fieldKey]
}
})
})
}
} catch (e) {
console.error('Failed to load detail:', e)
ElMessage.error('加载详情失败')
} finally {
detailLoading.value = false
}
}
}
},
{ immediate: true, deep: true }
)
const close = () => {
isFullscreen.value = false
resetFormData()
emit('close')
}
async function handleTranslate() {
if (!primaryLocale.value || !activeSecondaryLocale.value || translating.value) return
const fromKey = primaryLocale.value.key
const toKey = activeSecondaryLocale.value.key
const texts: { key: string; text: string }[] = []
translateFields.value.forEach((item) => {
const val = formData.translations?.[fromKey]?.[item.key]
if (val != null && String(val).trim()) {
texts.push({ key: item.key, text: String(val).trim() })
}
})
if (texts.length === 0) return
translating.value = true
try {
const results = await Promise.allSettled(
texts.map(async ({ key, text }) => {
const res = await props.translateApi({ text, from: fromKey, to: toKey })
return { key, translated: parseTranslateResult(res) }
})
)
let failCount = 0
for (const result of results) {
if (result.status === 'fulfilled') {
const { key, translated } = result.value
if (formData.translations[toKey]) {
formData.translations[toKey][key] = translated
}
} else {
failCount++
}
}
if (failCount > 0) {
ElMessage.warning(`${failCount} 个字段翻译失败`)
}
} catch (e) {
console.error('Translate failed:', e)
ElMessage.error('翻译失败')
} finally {
translating.value = false
}
}
async function handleTranslateField(itemKey: string) {
if (!primaryLocale.value || !activeSecondaryLocale.value || translatingFieldKey.value) return
const fromKey = primaryLocale.value.key
const toKey = activeSecondaryLocale.value.key
const text = formData.translations?.[fromKey]?.[itemKey]
if (text == null || !String(text).trim()) return
translatingFieldKey.value = itemKey
try {
const res = await props.translateApi({ text: String(text).trim(), from: fromKey, to: toKey })
if (formData.translations[toKey]) formData.translations[toKey][itemKey] = parseTranslateResult(res)
} catch (e) {
console.error('Translate failed:', e)
ElMessage.error('翻译失败')
} finally {
translatingFieldKey.value = ''
}
}
async function submit() {
try {
await formRef.value?.validate()
} catch {
return
}
const data: Record<string, any> = { id: props.id || '' }
normalFields.value.forEach((item) => {
data[item.key] = formData[item.key]
})
if (Object.keys(formData.translations || {}).length > 0) {
data.translations = formData.translations
}
emit('submit', data)
}
</script>
<style scoped lang="scss">
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding-right: 40px;
.dialog-title {
font-size: 18px;
font-weight: 500;
}
.dialog-header-actions {
display: flex;
gap: 4px;
}
}
.in18-form-header {
display: flex;
border-bottom: 1px solid var(--el-border-color);
.in18-form-header-left,
.in18-form-header-right {
flex: 1;
padding: 15px;
font-weight: 500;
}
.in18-form-header-right {
display: flex;
gap: 8px;
align-items: center;
.locale-tab {
padding: 5px 12px;
border: 1px solid var(--el-border-color);
border-radius: 5px;
cursor: pointer;
font-size: 14px;
&.active {
background-color: var(--el-color-primary);
color: #fff;
border-color: var(--el-color-primary);
}
&:hover:not(.active) {
background-color: var(--el-fill-color-light);
}
}
}
}
.in18-form-top {
padding: 15px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
:deep(.el-form-item) {
width: 100%;
max-width: 400px;
margin-bottom: 0;
}
}
.in18-form-wrapper {
display: flex;
flex-direction: row;
min-height: 200px;
border-bottom: 1px solid var(--el-border-color);
.in18-form-left,
.in18-form-right {
flex: 1;
padding: 15px;
overflow-y: auto;
:deep(.el-form-item) {
margin-bottom: 16px;
}
}
.in18-form-left {
border-right: 1px solid var(--el-border-color);
}
.field-with-translate {
display: flex;
align-items: flex-start;
gap: 8px;
width: 100%;
.el-input,
.el-textarea {
flex: 1;
}
.translate-icon {
flex-shrink: 0;
padding: 8px 0;
}
}
.in18-form-center {
display: flex;
align-items: center;
justify-content: center;
padding: 0 15px;
min-width: 80px;
border-right: 1px solid var(--el-border-color);
}
}
</style>

View File

@ -0,0 +1,364 @@
<template>
<el-dialog :model-value="show" width="900" :fullscreen="isFullscreen" :close-on-click-modal="false" @close="close"
:show-close="false">
<template #header>
<div class="jfd-header">
<span class="jfd-title">{{ title }}</span>
<div class="jfd-header-actions">
<el-button link circle @click="isFullscreen = !isFullscreen"
:title="isFullscreen ? '退出全屏' : '全屏'">
<el-icon>
<CopyDocument v-if="isFullscreen" />
<FullScreen v-else />
</el-icon>
</el-button>
<el-button link circle @click="close" title="关闭">
<el-icon size="18">
<Close />
</el-icon>
</el-button>
</div>
</div>
</template>
<el-tabs v-if="langKeys.length" v-model="activeTab" type="border-card">
<el-tab-pane v-for="lang in langKeys" :key="lang" :label="lang" :name="lang">
<el-scrollbar max-height="60vh">
<div class="jfd-content">
<NodeEditor v-for="sKey in getSectionKeys(lang)" :key="sKey" :parent="formData[lang]"
:field-key="sKey" :field-map="fieldMap" :depth="0" />
</div>
</el-scrollbar>
</el-tab-pane>
</el-tabs>
<el-empty v-else description="无数据" />
<template #footer>
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="submit">保存</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch, defineComponent, h, type PropType, type VNode, type Component } from 'vue'
import { Close, FullScreen, CopyDocument, Plus, Delete } from '@element-plus/icons-vue'
import {
ElFormItem, ElInput, ElInputNumber, ElSwitch, ElButton,
ElCollapse, ElCollapseItem, ElIcon
} from 'element-plus'
const props = withDefaults(defineProps<{
show: boolean
title?: string
data: Record<string, any>
fieldMap?: Record<string, string>
}>(), {
show: false,
title: '配置编辑',
data: () => ({}),
fieldMap: () => ({}),
})
const emit = defineEmits(['close', 'submit'])
const isFullscreen = ref(false)
const formData = ref<Record<string, any>>({})
const activeTab = ref('')
const langKeys = computed(() => Object.keys(formData.value))
function getSectionKeys(lang: string) {
const langData = formData.value[lang]
return langData && typeof langData === 'object' ? Object.keys(langData) : []
}
const itemTemplates: Record<string, any> = {}
function collectItemTemplates(data: any) {
if (data === null || data === undefined) return
if (Array.isArray(data)) {
if (data.length > 0 && typeof data[0] === 'object' && !Array.isArray(data[0])) {
for (const key of Object.keys(data[0])) {
collectItemTemplates(data[0][key])
}
}
data.forEach(item => collectItemTemplates(item))
} else if (typeof data === 'object') {
for (const key of Object.keys(data)) {
const val = data[key]
if (Array.isArray(val) && val.length > 0 && typeof val[0] === 'object' && !Array.isArray(val[0])) {
if (!itemTemplates[key]) {
itemTemplates[key] = createEmpty(val[0])
}
}
collectItemTemplates(val)
}
}
}
watch(() => props.show, (val) => {
if (val && props.data) {
formData.value = JSON.parse(JSON.stringify(props.data))
Object.keys(itemTemplates).forEach(k => delete itemTemplates[k])
const firstLang = langKeys.value[0]
if (firstLang) collectItemTemplates(formData.value[firstLang])
activeTab.value = firstLang || ''
}
}, { immediate: true })
function close() {
isFullscreen.value = false
emit('close')
}
function submit() {
emit('submit', JSON.parse(JSON.stringify(formData.value)))
}
function createEmpty(template: any): any {
if (typeof template === 'string') return ''
if (typeof template === 'number') return 0
if (typeof template === 'boolean') return false
if (Array.isArray(template)) return []
if (typeof template === 'object' && template !== null) {
const obj: any = {}
for (const k of Object.keys(template)) obj[k] = createEmpty(template[k])
return obj
}
return ''
}
const NodeEditor: Component = defineComponent({
name: 'NodeEditor',
props: {
parent: { type: Object as PropType<any>, required: true },
fieldKey: { type: [String, Number] as PropType<string | number>, required: true },
fieldMap: { type: Object as PropType<Record<string, string>>, default: () => ({}) },
depth: { type: Number, default: 0 },
},
setup(p) {
return () => {
const value = p.parent[p.fieldKey]
const key = p.fieldKey
const fm = p.fieldMap
const depth = p.depth
const label = typeof key === 'string' ? (fm[key] || key) : `#${Number(key) + 1}`
if (value === null || value === undefined) return null
if (typeof value === 'boolean') {
return h(ElFormItem, { label, class: 'jfd-form-item' }, () =>
h(ElSwitch, {
modelValue: value,
'onUpdate:modelValue': (v: any) => { p.parent[key] = v },
})
)
}
if (typeof value === 'number') {
return h(ElFormItem, { label, class: 'jfd-form-item' }, () =>
h(ElInputNumber, {
modelValue: value,
'onUpdate:modelValue': (v: number | undefined) => { p.parent[key] = v ?? 0 },
controlsPosition: 'right',
})
)
}
if (typeof value === 'string') {
const isLong = value.length > 50
return h(ElFormItem, { label, class: 'jfd-form-item' }, () =>
h(ElInput, {
modelValue: value,
...(isLong ? { type: 'textarea', autosize: { minRows: 2, maxRows: 6 } } : {}),
'onUpdate:modelValue': (v: string) => { p.parent[key] = v },
})
)
}
if (Array.isArray(value)) {
const keyStr = String(key)
const hasLiveObj = value.length > 0 && typeof value[0] === 'object' && !Array.isArray(value[0])
const savedTemplate = itemTemplates[keyStr]
const isObjArr = hasLiveObj || !!savedTemplate
if (!isObjArr) {
return h('div', { class: 'jfd-array-group' }, [
h('div', { class: 'jfd-group-header' }, [
h('span', { class: 'jfd-group-title' }, label),
h(ElButton, {
size: 'small', type: 'primary', link: true,
onClick: () => value.push(''),
}, () => [h(ElIcon, { size: 14 }, () => h(Plus)), ' 添加'])
]),
...value.map((_: any, i: number) =>
h('div', { class: 'jfd-string-item', key: i }, [
h(ElInput, {
modelValue: value[i],
'onUpdate:modelValue': (v: string) => { value[i] = v },
}),
h(ElButton, {
size: 'small', type: 'danger', link: true,
onClick: () => value.splice(i, 1),
}, () => h(ElIcon, { size: 14 }, () => h(Delete)))
])
)
])
}
const makeNewItem = () => {
const tpl = hasLiveObj ? value[0] : savedTemplate
return JSON.parse(JSON.stringify(tpl ? createEmpty(tpl) : {}))
}
return h('div', { class: 'jfd-array-group' }, [
h('div', { class: 'jfd-group-header' }, [
h('span', { class: 'jfd-group-title' }, label),
h(ElButton, {
size: 'small', type: 'primary', link: true,
onClick: () => value.push(makeNewItem()),
}, () => [h(ElIcon, { size: 14 }, () => h(Plus)), ' 添加'])
]),
h(ElCollapse, { class: 'jfd-obj-array' }, () =>
value.map((item: any, i: number) => {
const tag = item.title || item.name || item.text || ''
return h(ElCollapseItem, {
title: `${label} #${i + 1}${tag ? ' - ' + tag : ''}`,
name: `${String(key)}-${i}`,
key: i,
}, () => [
...Object.keys(item).map(k =>
h(NodeEditor, { parent: item, fieldKey: k, fieldMap: fm, depth: depth + 1, key: k })
),
h('div', { class: 'jfd-item-actions' },
h(ElButton, {
size: 'small', type: 'danger',
onClick: () => value.splice(i, 1),
}, () => [h(ElIcon, { size: 14 }, () => h(Delete)), ' 删除此项'])
)
])
})
)
])
}
if (typeof value === 'object') {
const children = Object.keys(value).map(k =>
h(NodeEditor, { parent: value, fieldKey: k, fieldMap: fm, depth: depth + 1, key: k })
)
if (depth === 0) {
return h('div', { class: 'jfd-section' }, [
h('div', { class: 'jfd-section-header' }, label),
h('div', { class: 'jfd-section-body' }, children)
])
}
return h('div', { class: 'jfd-nested' }, [
h('div', { class: 'jfd-nested-title' }, label),
...children
])
}
return null
}
}
})
</script>
<style lang="scss">
.jfd-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.jfd-title {
font-size: 18px;
font-weight: 500;
}
.jfd-header-actions {
display: flex;
gap: 4px;
}
}
.jfd-content {
padding: 12px;
}
.jfd-form-item {
margin-bottom: 16px;
}
.jfd-section {
margin-bottom: 16px;
border: 1px solid var(--el-border-color);
border-radius: 6px;
overflow: hidden;
}
.jfd-section-header {
padding: 10px 16px;
font-weight: 600;
font-size: 15px;
background-color: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color);
}
.jfd-section-body {
padding: 16px;
}
.jfd-array-group {
margin-bottom: 12px;
}
.jfd-group-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.jfd-group-title {
font-weight: 500;
font-size: 14px;
color: var(--el-text-color-primary);
}
.jfd-string-item {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 8px;
}
.jfd-item-actions {
text-align: right;
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed var(--el-border-color-lighter);
}
.jfd-nested {
margin-left: 12px;
padding-left: 12px;
border-left: 2px solid var(--el-border-color);
margin-bottom: 12px;
}
.jfd-nested-title {
font-weight: 500;
font-size: 13px;
color: var(--el-text-color-regular);
margin-bottom: 8px;
}
.jfd-obj-array .el-collapse-item__header {
font-size: 13px;
}
</style>

25
config.json Normal file
View File

@ -0,0 +1,25 @@
{
"title": "工商年报",
"baseUrl": "/companyHome",
"tokenKey": "token",
"messageDuration": 3000,
"requestTimeout": 60000,
"successCode": [
200,
0
],
"invalidCode": [
1001106,
1001003,
11009
],
"menus":false,
"noLogin": false,
"mainColor": "#1677ff",
"port": 9290 ,
"noPermissionCode": 401,
"rbBuldInfo": {
"api": "http://rb2.batiao8.com/console/custom/deploy/f5b5219cce4a7d47a60416ef4d3fa891",
"key": "bNJXlzwc"
}
}

23
data/const.ts Normal file
View File

@ -0,0 +1,23 @@
// 语言
export const LOCALES = [
{
name: '中文',
key: 'ZH',
isPrimary: true,
},
{
name: 'English',
key: 'EN',
},
];
// 分类
export const categoryTypes = [
{ key: 'process', name: '历程', path: '/history' },
{ key: 'news', name: '新闻', path: '/news' },
{ key: 'file', name: '文件', path: '/document' },
{ key: 'job', name: '招聘', path: '/jobs' },
{ key: 'job_type', name: '职业类型', path: '/jobs' },
{ key: 'job_area', name: '业务领域', path: '/jobs' },
{ key: 'job_unit', name: '所属板块', path: '/jobs' },
]

28
layout/TopLayout.scss Normal file
View File

@ -0,0 +1,28 @@
.upload-flie {
.el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 64px;
height: 64px;
text-align: center;
}
.uploaded-img {
width: 64px;
height: 64px;
display: block;
}
}

232
layout/TopLayout.tsx Normal file
View File

@ -0,0 +1,232 @@
import type { TopLayoutType } from 'lib/type/TopLayout'
import { ElMessage } from 'element-plus'
import { indexStore } from 'lib/stores'
import login from 'src/api/login'
import clipboard from 'clipboard'
import 'src/layout/TopLayout.scss'
import TableLayout from 'lib/layout/TableLayout.vue'
import message from 'lib/utils/message'
import type { Form, FormData } from 'lib/type/TableData'
import { formatConfig } from 'src/tools'
// const TableLayout: any = utils.deepClone(tl)
const store = indexStore()
const data: TopLayoutType = {
userButtons: [
// {
// name: '修改配置',
// type: 'success',
// icon: 'EditPen',
// onClick(self) {
// self.methods?.getConfig(self)
// ;(self.bean as any).configForm.show = 'form'
// }
// // getHide() {
// // return store.$state.kwargs.role != 1
// // }
// },
// {
// name: '用户信息',
// type: 'success',
// icon: 'User',
// onClick(self) {
// ;(self.bean as any).userInfoDialogShow = true
// }
// },
// {
// name: '修改密码',
// type: 'warning',
// icon: 'Key',
// onClick(self) {
// ;(self.bean as any).resetPasswordDialogShow = true
// }
// }
],
bean: {
userInfoDialogShow: false,
resetPasswordDialogShow: false,
resetPasswordInput: '',
resetPasswordInputOld: '',
resetPasswordInputTwo: '',
configForm: {
hideButton: true,
show: false,
data: [],
async subFun(self, data) {
// console.log(data)
for (const key in data){
if (typeof data[key] === "object" && Array.isArray(data[key])){
data[key] = data[key].join(",")
}
}
return login.editorConfig(data)
}
} as Form
},
methods: {
handleEditorConfigSubmit(self: TopLayoutType) {
login.editorConfig((self.bean as any).editorConfigInfo).then((res: any) => {
if (res.code == 0) {
ElMessage.success('配置修改成功')
} else {
ElMessage.success('配置修改失败')
}
self.methods?.updateConfig(self)
})
},
handleResetPasswordSubmit(self: TopLayoutType) {
if ((self.bean as any).resetPasswordInputOld === '') {
ElMessage.error('旧密码不能为空')
return
}
if ((self.bean as any).resetPasswordInput === '') {
ElMessage.error('密码不能为空')
return
}
if ((self.bean as any).resetPasswordInput !== (self.bean as any).resetPasswordInputTwo) {
ElMessage.error('两次输入的密码不一致')
return
}
login
.resetPassWord({
old_password: (self.bean as any).resetPasswordInputOld,
password: (self.bean as any).resetPasswordInput
})
.then((res: any) => {
if (res.code === 0) {
ElMessage.success('密码修改成功')
setTimeout(() => {
store.logout()
}, 1000)
} else {
ElMessage.error(res.message)
}
})
},
copy(text: string) {
clipboard.copy(text)
message.success('复制成功')
},
async getConfig(self: TopLayoutType) {
const res = await login.getConfig()
let formData: FormData[] = res.data
formData = formatConfig(formData)
;(self.bean as any).configForm.data = formData
}
},
launchTask: [
// async (self) => {
// const res = await login.getHomeData({})
// ;(self.bean as any).homeData = res.data
// }
],
addNods: [
(self) => {
return (
<el-dialog width="500" append-to-body vModel={(self.bean as any).userInfoDialogShow}>
<el-form label-width="100">
<el-form-item label="名称:">
<div>{store.$state.kwargs.realname}</div>
</el-form-item>
<el-form-item label="角色:">
<div>{store.$state.kwargs.role_name}</div>
</el-form-item>
<el-form-item label="级别:">
<div>{store.$state.kwargs.level_name}</div>
</el-form-item>
<el-form-item label="上级名称:">
<div>{store.$state.kwargs.parent_info.realname}</div>
</el-form-item>
{/* <el-form-item label="">
<div>{store.$state.kwargs.parent_info.role_name}</div>
</el-form-item>
<el-form-item label="上级级别:">
<div>{store.$state.kwargs.parent_info.level_name}</div>
</el-form-item> */}
{/*<el-form-item label="分享二维码:">*/}
{/* <img style={{*/}
{/* width:"256px",*/}
{/* height:"256px"*/}
{/* }} src={'data:image/png;base64,' + self.bean?.homeData.qrcode} />*/}
{/*</el-form-item>*/}
{/*<el-form-item label="分享链接:">*/}
{/* <div class="flex-row" style={{*/}
{/* alignItems:"center",*/}
{/* columnGap:"10px"*/}
{/* }}>*/}
{/* <span>{self.bean?.homeData.url}</span>*/}
{/* <el-button*/}
{/* type="primary"*/}
{/* size="small"*/}
{/* onClick={() => {*/}
{/* self.methods?.copy(self.bean?.homeData.url)*/}
{/* }}*/}
{/* >*/}
{/* 复制*/}
{/* </el-button>*/}
{/* </div>*/}
{/*</el-form-item>*/}
</el-form>
</el-dialog>
)
},
(self) => {
return (
<el-dialog width="500" append-to-body vModel={(self.bean as any).resetPasswordDialogShow}>
<el-form label-width="80">
<el-form-item label="旧密码">
<el-input
type="password"
vModel={(self.bean as any).resetPasswordInputOld}
></el-input>
</el-form-item>
<el-form-item label="新密码">
<el-input type="password" vModel={(self.bean as any).resetPasswordInput}></el-input>
</el-form-item>
<el-form-item label="再次输入">
<el-input
type="password"
vModel={(self.bean as any).resetPasswordInputTwo}
></el-input>
</el-form-item>
<el-form-item>
<el-button
onClick={() => {
;(self.bean as any).resetPasswordDialogShow = false
}}
>
</el-button>
<el-button
type="primary"
onClick={() => {
;(self.methods as any).handleResetPasswordSubmit(self)
}}
>
</el-button>
</el-form-item>
</el-form>
</el-dialog>
)
},
(self) => {
let element: any = <></>
if (self.bean && TableLayout.methods) {
element = TableLayout.methods.createForm.call(
{
...TableLayout.methods,
...self.this_,
fetchData: () => {}
},
self.bean.configForm,
{
id: 'form'
}
)
}
return element
}
]
}
export default data

12
package.json Normal file
View File

@ -0,0 +1,12 @@
{
"name": "yintai-company-home-am",
"version": "1.0.0",
"main": "index.js",
"repository": "https://git.u8t.cn/batiao/corp-bm.git",
"author": "YSASM <1613921123@qq.com>",
"license": "MIT",
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1"
},
"lib": "cy-admin"
}

12
proxy.mjs Normal file
View File

@ -0,0 +1,12 @@
import dotenv from "dotenv"
dotenv.config()
// eslint-disable-next-line no-undef
const isDev = process.env.DEV
console.log("isDev:" + isDev)
export default {
'/companyHome': {
target: isDev ? "http://10.3.0.7:9999/":'https://companyapi.batiao8.com/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/companyHome/, '')
}
}

BIN
pub/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
pub/login.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
pub/oderDetailCallBack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

1
pub/svg/交易管理.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="24" height="24" viewBox="0 0 24 24"><defs><clipPath id="master_svg0_23_247"><rect x="0" y="0" width="24" height="24" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_23_247)"><g><path d="M12.00706015625,1.47662353515625C6.21137015625,1.47662353515625,1.51416015625,6.17383353515625,1.51416015625,11.96952353515625C1.51416015625,17.76522353515625,6.21137015625,22.46242353515625,12.00706015625,22.46242353515625C17.80276015625,22.46242353515625,22.49996015625,17.76522353515625,22.49996015625,11.96952353515625C22.49996015625,6.17383353515625,17.80276015625,1.47662353515625,12.00706015625,1.47662353515625ZM16.18026015625,10.95350353515625C16.18026015625,11.39236353515625,15.85046015625,11.72482353515625,15.40896015625,11.72482353515625L12.89016015625,11.72482353515625L12.89016015625,13.29142353515625L15.40896015625,13.29142353515625C15.84786015625,13.29142353515625,16.18026015625,13.62132353515625,16.18026015625,14.06282353515625L16.18026015625,14.20112353515625C16.18026015625,14.64002353515625,15.85046015625,14.97242353515625,15.40896015625,14.97242353515625L12.89016015625,14.97242353515625L12.89016015625,16.65872353515625C12.89016015625,17.097623535156252,12.56036015625,17.43012353515625,12.11876015625,17.43012353515625L11.91926015625,17.43012353515625C11.51506015625,17.37422353515625,11.21180015625,17.02052353515625,11.21180015625,16.61622353515625L11.21180015625,14.97512353515625L8.599870156249999,14.97512353515625C8.16101015625,14.97512353515625,7.82853015625,14.64532353515625,7.82853015625,14.20382353515625L7.82853015625,14.06542353515625C7.82853015625,13.62662353515625,8.15835015625,13.29412353515625,8.599870156249999,13.29412353515625L11.21180015625,13.29412353515625L11.21180015625,11.72752353515625L8.599870156249999,11.72752353515625C8.16101015625,11.72752353515625,7.82853015625,11.39768353515625,7.82853015625,10.95616353515625L7.82853015625,10.81785353515625C7.82853015625,10.37898353515625,8.15835015625,10.04650353515625,8.599870156249999,10.04650353515625L11.23574015625,10.04650353515625L9.254180156250001,7.93462353515625C8.940330156249999,7.62076353515625,8.96161015625,7.08614353515625,9.30206015625,6.74569353515625C9.61592015625,6.43183353515625,10.15054015625,6.45045353515625,10.49365015625,6.79356353515625L10.49631015625,6.79622353515625L12.04166015625,8.445303535156249L13.65886015625,6.93187353515625C13.97266015625,6.61802353515625,14.50726015625,6.63930353515625,14.84776015625,6.97975353515625L14.85576015625,6.98773353515625C15.13766015625,7.32553353515625,15.11376015625,7.81227353515625,14.80256015625,8.12347353515625L14.79986015625,8.12613353515625L12.74916015625,10.04916353515625L15.41166015625,10.04916353515625C15.85046015625,10.04916353515625,16.182960156249997,10.37898353515625,16.182960156249997,10.82051353515625L16.18026015625,10.95350353515625Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

1
pub/svg/人员管理.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="24" height="24" viewBox="0 0 24 24"><defs><clipPath id="master_svg0_23_282"><rect x="0" y="0" width="24" height="24" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_23_282)"><g><path d="M7.2913005859375,8.84384478515625C6.1767905859375,6.23605478515625,7.4064805859375,3.2201447851562497,10.026700585937501,2.13513078515625C12.6469105859375,1.05011978515625,15.6487505859375,2.31378178515625,16.704150585937498,4.9460747851562505C17.728750585937497,7.53348478515625,16.487450585937502,10.46435478515625,13.9160505859375,11.52872478515625C11.3447705859375,12.59305478515625,8.395130585937501,11.39702478515625,7.2913005859375,8.84238478515625L7.2913005859375,8.84384478515625ZM20.4850505859375,22.25375478515625C20.9145505859375,22.25375478515625,21.2607505859375,21.88855478515625,21.2257505859375,21.45755478515625C20.8750505859375,17.19895478515625,17.7676505859375,13.74085478515625,13.7296505859375,12.95925478515625C13.5885505859375,12.93365478515625,13.4445505859375,12.98485478515625,13.3512505859375,13.09375478515625Q12.3271505859375,14.29315478515625,12.1167705859375,14.55025478515625C12.0943405859375,14.57945478515625,12.0888005859375,14.61825478515625,12.1021605859375,14.65255478515625L13.6653505859375,18.71975478515625C13.6945505859375,18.78845478515625,13.6799505859375,18.86875478515625,13.6317505859375,18.92425478515625L12.0641805859375,21.15515478515625C12.0326505859375,21.19805478515625,11.9686005859375,21.19805478515625,11.9370805859375,21.15515478515625L10.3694905859375,18.92285478515625C10.3211205859375,18.86625478515625,10.3081705859375,18.78745478515625,10.3358905859375,18.71835478515625L11.8990905859375,14.65105478515625C11.9137205859375,14.61695478515625,11.9080905859375,14.57745478515625,11.8844805859375,14.54875478515625Q11.6741105859375,14.29165478515625,10.6499905859375,13.09225478515625C10.557260585937499,12.98255478515625,10.412740585937499,12.93125478515625,10.2716105859375,12.95785478515625C6.230670585937499,13.73945478515625,3.1203355859375,17.19745478515625,2.7740944959375,21.45605478515625C2.7408270859375,21.88645478515625,3.0817325859375,22.25355478515625,3.5133275859375,22.25235478515625L20.4850505859375,22.25235478515625L20.4850505859375,22.25375478515625Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

1
pub/svg/卡号列表.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="24" height="24" viewBox="0 0 24 24"><defs><clipPath id="master_svg0_30_116"><rect x="0" y="0" width="24" height="24" rx="0"/></clipPath><clipPath id="master_svg1_30_016"><rect x="0.625" y="0.625" width="22.75" height="22.75" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_30_116)"><g clip-path="url(#master_svg1_30_016)"></g><g><path d="M22.625,5.35937201171875L22.625,7.13019201171875L1.375,7.13019201171875L1.375,5.35895201171875C1.37765625,4.38232401171875,2.055872,3.59165413171875,2.89259,3.58856201171875L21.1078,3.58856201171875C21.9445,3.59165413171875,22.6223,4.38278101171875,22.625,5.35937201171875ZM22.625,8.45832201171875L22.625,18.70396201171875C22.6223,19.64596201171875,21.9445,20.40836201171875,21.1074,20.41146201171875L2.8921799999999998,20.41146201171875C2.055457,20.40836201171875,1.3776355,19.64556201171875,1.375,18.70396201171875L1.375,8.45832201171875L22.625,8.45832201171875ZM3.39551,12.87303201171875L6.75038,12.87303201171875C7.13201,12.87303201171875,7.44233,12.57951201171875,7.44233,12.21517201171875C7.44233,11.85215201171875,7.13244,11.55731201171875,6.75038,11.55731201171875L3.39551,11.55731201171875C3.01301,11.55729201171875,2.70312,11.85213201171875,2.70312,12.21515201171875C2.70312,12.57949201171875,3.01301,12.87300201171875,3.39551,12.87303201171875ZM8.070509999999999,14.08116201171875L3.39508,14.08116201171875C3.01303,14.08076201171875,2.70312,14.37556201171875,2.70312,14.73906201171875C2.70312,15.10336201171875,3.01301,15.39686201171875,3.39551,15.39686201171875L8.070509999999999,15.39686201171875C8.452580000000001,15.39686201171875,8.762920000000001,15.10296201171875,8.762920000000001,14.73906201171875C8.762920000000001,14.37556201171875,8.453009999999999,14.08116201171875,8.07008,14.08116201171875L8.070509999999999,14.08116201171875Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

1
pub/svg/商品管理.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="24" height="24" viewBox="0 0 24 24"><defs><clipPath id="master_svg0_23_261"><rect x="0" y="0" width="24" height="24" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_23_261)"><g><path d="M18.47168671875,2.52362060546875L5.71448671875,2.52362060546875C4.00208671875,2.52362060546875,2.60888671875,3.91682060546875,2.60888671875,5.62922060546875L2.60888671875,18.38642060546875C2.60888671875,20.09882060546875,4.00208671875,21.49202060546875,5.71448671875,21.49202060546875L18.47168671875,21.49202060546875C20.18408671875,21.49202060546875,21.57728671875,20.09882060546875,21.57728671875,18.38642060546875L21.57728671875,5.62922060546875C21.57608671875,3.91682060546875,20.18288671875,2.52362060546875,18.47168671875,2.52362060546875ZM8.330486718749999,6.50042060546875C8.74088671875,6.36722060546875,9.180086718750001,6.59162060546875,9.31328671875,7.00082060546875C9.702886718750001,8.20590060546875,10.82599671875,9.02156060546875,12.09248671875,9.01922060546875C13.36328671875,9.01922060546875,14.48048671875,8.20802060546875,14.87288671875,7.00082060546875C15.00608671875,6.59162060546875,15.44648671875,6.36722060546875,15.85568671875,6.49922060546875C16.26488671875,6.63122060546875,16.48928671875,7.07162060546875,16.35728671875,7.48202060546875C15.75608671875,9.33482060546875,14.04248671875,10.57922060546875,12.09368671875,10.57922060546875C10.15081671875,10.58304060546875,8.427796718749999,9.331870605468751,7.83008671875,7.48322060546875C7.69568671875,7.07402060546875,7.92008671875,6.63362060546875,8.330486718749999,6.50042060546875ZM15.90968671875,18.174020605468748L8.27648671875,18.174020605468748C7.84570671875,18.174020605468748,7.49648671875,17.82482060546875,7.49648671875,17.39402060546875C7.49648671875,16.96322060546875,7.84570671875,16.614020605468752,8.27648671875,16.614020605468752L15.90968671875,16.614020605468752C16.34048671875,16.614020605468752,16.68968671875,16.96322060546875,16.68968671875,17.39402060546875C16.68968671875,17.82482060546875,16.34048671875,18.174020605468748,15.90968671875,18.174020605468748Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

1
pub/svg/用户列表.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

1
pub/svg/订单管理.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.0 KiB

1
pub/svg/订单统计.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="24" height="24" viewBox="0 0 24 24"><defs><clipPath id="master_svg0_25_023"><rect x="0" y="0" width="24" height="24" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_25_023)"><g><path d="M20.3684552734375,2.87506103515625C21.5060552734375,2.87506103515625,22.4283552734375,3.79731803515625,22.4283552734375,4.93498103515625L22.4283552734375,15.58886103515625C22.4283552734375,16.72656103515625,21.5060552734375,17.648761035156248,20.3684552734375,17.648761035156248L3.6315752734375,17.648761035156248C2.4939132734375002,17.648761035156248,1.5716555386445,16.72656103515625,1.5716552734375,15.58886103515625L1.5716552734375,4.93498103515625C1.5716555386445,3.79731803515625,2.4939132734375002,2.87506103515625,3.6315752734375,2.87506103515625L20.3684552734375,2.87506103515625ZM19.1736552734375,6.78633103515625C18.8587552734375,6.42393103515625,18.3096552734375,6.3856510351562505,17.9474552734375,6.70085103515625L13.8528552734375,10.26245103515625L11.3099252734375,7.72720103515625L11.2537952734375,7.67570103515625C10.9217452734375,7.39824103515625,10.4361952734375,7.40724103515625,10.1146552734375,7.69682103515625L5.1708552734375,12.14985103515625L5.117295273437501,12.20238103515625C4.8094752734375,12.53210103515625,4.8050052734375,13.04246103515625,5.1069952734375,13.37756103515625L5.159525273437501,13.43116103515625C5.4891752734375,13.73866103515625,5.9991952734375,13.74306103515625,6.3341952734375,13.44146103515625L10.6651752734375,9.53942103515625L13.1988552734375,12.06539103515625L13.2539552734375,12.11638103515625C13.5784552734375,12.38829103515625,14.0573552734375,12.38829103515625,14.3828552734375,12.10608103515625L19.0881552734375,8.01199103515625L19.1427552734375,7.96049103515625C19.4533552734375,7.64017103515625,19.4718552734375,7.12931103515625,19.1736552734375,6.78633103515625ZM6.3517052734375,19.38686103515625L17.6488552734375,19.38686103515625Q18.5181552734375,19.38686103515625,18.5181552734375,20.25616103515625L18.5181552734375,20.25566103515625Q18.5181552734375,21.12496103515625,17.6488552734375,21.12496103515625L6.3517052734375,21.12496103515625Q5.482425273437499,21.12496103515625,5.482425273437499,20.25566103515625L5.482425273437499,20.25616103515625Q5.482425273437499,19.38686103515625,6.3517052734375,19.38686103515625Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

1
pub/svg/配置.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="24" height="24" viewBox="0 0 24 24"><defs><clipPath id="master_svg0_26_214"><rect x="0" y="0" width="24" height="24" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_26_214)"><g><path d="M20.65235859375,12.00840673828125C20.65285859375,11.10123673828125,20.96205859375,10.22121673828125,21.52905859375,9.51307673828125C21.75295859375,9.18208673828125,21.75295859375,8.74814673828125,21.52905859375,8.41714673828125L19.84305859375,5.60145673828125C19.63985859375,5.22094673828125,19.22675859375,5.001076738281251,18.79765859375,5.04506673828125C16.986358593749998,5.24947673828125,15.26625859375,4.2021667382812495,14.61625859375,2.49914073828125C14.46275859375,2.12427373828125,14.09395859375,1.88288943828125,13.68895859375,1.89216503728125L10.70468859375,1.89216503728125C9.99654859375,1.89216503728125,9.62561859375,2.12821073828125,9.49072859375,2.49914073828125C8.81391859375,4.29947673828125,6.95056859375,5.36222673828125,5.05643859375,5.02820673828125C4.64990859375,4.96186673828125,4.24538859375,5.15738673828125,4.04481859375,5.517156738281249L2.35877259375,8.31598673828125C2.12715409375,8.650756738281249,2.12715409375,9.09400673828125,2.35877259375,9.42877673828125C3.60826859375,10.91521673828125,3.60826859375,13.08480673828125,2.35877259375,14.57120673828125C2.12715469375,14.90600673828125,2.12715469375,15.34920673828125,2.35877259375,15.68400673828125L4.04481859375,18.48280673828125C4.24538859375,18.84260673828125,4.64990859375,19.03810673828125,5.05643859375,18.97180673828125C6.95056859375,18.63770673828125,8.81391859375,19.70050673828125,9.49072859375,21.50080673828125C9.64428859375,21.87570673828125,10.013068593749999,22.11710673828125,10.41805859375,22.10780673828125L13.38545859375,22.10780673828125C14.09365859375,22.10780673828125,14.46455859375,21.87180673828125,14.59945859375,21.50080673828125C15.26125859375,19.74340673828125,17.05745859375,18.68390673828125,18.91575859375,18.95490673828125C19.34485859375,18.99890673828125,19.75785859375,18.77900673828125,19.96105859375,18.39850673828125L21.64705859375,15.58280673828125C21.87105859375,15.25180673828125,21.87105859375,14.81790673828125,21.64705859375,14.48690673828125C21.04105859375,13.79930673828125,20.68985859375,12.92420673828125,20.65235859375,12.00840673828125ZM12.00293859375,16.07180673828125C9.75880859375,16.07180673828125,7.93957859375,14.25250673828125,7.93957859375,12.00840673828125C7.93957859375,9.76428673828125,9.75880859375,7.94505673828125,12.00293859375,7.94505673828125C14.24705859375,7.94505673828125,16.06625859375,9.76428673828125,16.06625859375,12.00840673828125C16.06625859375,14.25250673828125,14.24705859375,16.07180673828125,12.00293859375,16.07180673828125Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

1
pub/svg/预填信息.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="24" height="24" viewBox="0 0 24 24"><defs><clipPath id="master_svg0_26_027"><rect x="0" y="0" width="24" height="24" rx="0"/></clipPath></defs><g><g></g><g clip-path="url(#master_svg0_26_027)"><g><path d="M21.13432109375,2.11511928125C20.72432109375,1.72154628125,20.16822109375,1.50048828125,19.58842109375,1.50048828125L4.28533109375,1.50048828125C3.70560109375,1.50048828125,3.14944109375,1.72154628125,2.73948709375,2.11511928125C2.32953309375,2.50876828125,2.09912109375,3.04259828125,2.09912109375,3.59918828125L2.09912109375,20.38888828125C2.09912109375,21.54798828125,3.07799309375,22.48758828125,4.28533109375,22.48758828125L19.58842109375,22.48758828125C20.16822109375,22.48758828125,20.72432109375,22.26648828125,21.13432109375,21.87288828125C21.54422109375,21.47928828125,21.77452109375,20.94548828125,21.77452109375,20.38888828125L21.77452109375,3.59920828125C21.77452109375,3.04259828125,21.54422109375,2.50878828125,21.13432109375,2.11511928125ZM14.12302109375,17.24088828125L6.47140109375,17.24088828125C5.86780109375,17.24088828125,5.37844109375,16.77098828125,5.37844109375,16.191488281250003C5.37844109375,15.61188828125,5.867811093749999,15.14218828125,6.47140109375,15.14218828125L14.12302109375,15.14218828125C14.72662109375,15.14218828125,15.21612109375,15.61188828125,15.21612109375,16.191488281250003C15.21612109375,16.77098828125,14.72662109375,17.24088828125,14.12302109375,17.24088828125ZM17.40242109375,12.51878828125L6.47140109375,12.51878828125C5.86780109375,12.51878828125,5.37844109375,12.04888828125,5.37844109375,11.46930828125C5.37844109375,10.88980828125,5.867811093749999,10.41991828125,6.47140109375,10.41991828125L17.40242109375,10.41991828125C18.00582109375,10.41991828125,18.49532109375,10.88980828125,18.49532109375,11.46930828125C18.49532109375,12.04888828125,18.00582109375,12.51878828125,17.40242109375,12.51878828125ZM17.40242109375,7.79659828125L6.47140109375,7.79659828125C5.86780109375,7.79659828125,5.37844109375,7.32678828125,5.37844109375,6.74721828125C5.37844109375,6.16762828125,5.867811093749999,5.69790828125,6.47140109375,5.69790828125L17.40242109375,5.69790828125C18.00582109375,5.69790828125,18.49532109375,6.16762828125,18.49532109375,6.74721828125C18.49532109375,7.32678828125,18.00582109375,7.79659828125,17.40242109375,7.79659828125Z" fill="#FFFFFF" fill-opacity="1"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

1
pub/svg/首页.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="24" height="24" viewBox="0 0 24 24"><defs><clipPath id="master_svg0_23_269"><rect x="0" y="0" width="24" height="24" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_23_269)"><g><path d="M21.7877193359375,10.14785L13.4000193359375,2.0802680000000002C12.5966193359375,1.3065769999999999,11.3375393359375,1.3065769999999999,10.5320493359375,2.0802680000000002L2.1422733359375,10.14785C1.5212007359375,10.74561,1.3367863359375,11.6126,1.6568613359375,12.4117C1.9790563359375,13.2108,2.7145893359375,13.7068,3.5751893359375,13.7068L4.2662193359375,13.7068L4.2662193359375,19.9303C4.2662193359375,21.3526,5.423569335937501,22.51,6.8458893359375,22.51L10.2670893359375,22.51L10.2670893359375,16.6214C10.2670893359375,15.8753,10.8754493359375,15.2669,11.6216193359375,15.2669L12.5436193359375,15.2669C13.2898193359375,15.2669,13.8981193359375,15.8753,13.8981193359375,16.6214L13.8981193359375,22.0373L13.9002193359375,22.0373L13.9002193359375,22.5078L17.3193193359375,22.5078C18.7416193359375,22.5078,19.8990193359375,21.3505,19.8990193359375,19.9282L19.8990193359375,13.7068L20.3569193359375,13.7068C21.2175193359375,13.7068,21.9530193359375,13.2108,22.2752193359375,12.4117C22.5953193359375,11.6126,22.4087193359375,10.74561,21.7877193359375,10.14785Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

191
routes.ts Normal file
View File

@ -0,0 +1,191 @@
import type { RouteRecordRaw } from "vue-router";
import TableLayout from "lib/layout/TableLayout.vue";
import MainLayout from "lib/layout/MainLayout.vue";
const routes: Array<RouteRecordRaw> = [
{
meta: {
hidden: true,
},
path: "/login",
component: () => import("src/views/LoginPage.vue"),
children: [],
},
{
path: "/",
redirect: "/home",
component: MainLayout,
children: [
{
path: "home",
component: () => import("src/views/HomeView.vue"),
meta: {
title: "首页",
icon: "首页",
myico: true,
},
},
],
},
{
path: "/admin",
component: MainLayout,
meta: {
title: "账号管理",
icon: "用户列表",
myico: true,
},
children: [
{
path: "list",
component: TableLayout,
meta: {
title: "账号列表",
icon: "用户列表",
myico: true,
},
},
{
path: "role",
component: TableLayout,
meta: {
title: "角色管理",
icon: "用户列表",
myico: true,
},
},
],
},
{
path: "/history",
component: MainLayout,
meta: {
title: "银泰历程",
icon: "TrendCharts",
},
children: [
{
path: "list",
component: TableLayout,
meta: {
title: "银泰历程",
icon: "TrendCharts",
},
},
{
path: "typesManage",
component: TableLayout,
meta: {
title: "类型管理",
icon: "Setting",
},
},
],
},
{
path: "/news",
component: MainLayout,
meta: {
title: "新闻管理",
icon: "VideoCamera",
},
children: [
{
path: "list",
component: TableLayout,
meta: {
title: "新闻列表",
icon: "VideoCamera",
},
},
{
path: "typesManage",
component: TableLayout,
meta: {
title: "类型管理",
icon: "Setting",
},
},
],
},
{
path: "/jobs",
component: MainLayout,
meta: {
title: "招聘管理",
icon: "House",
},
children: [
{
path: "list",
component: TableLayout,
meta: {
title: "招聘列表",
icon: "Memo",
},
},
{
path: "typesManage",
component: TableLayout,
meta: {
title: "类型管理",
icon: "Setting",
},
},
],
},
{
path: "/document",
component: MainLayout,
meta: {
title: "文档管理",
icon: "Files",
},
children: [
{
path: "list",
component: TableLayout,
meta: {
title: "文档列表",
icon: "Files",
},
},
{
path: "typesManage",
component: TableLayout,
meta: {
title: "类型管理",
icon: "Setting",
},
},
],
},
{
path: "/config",
component: MainLayout,
meta: {
title: "配置管理",
icon: "Setting",
},
children: [
{
path: "list",
component: TableLayout,
meta: {
title: "页面配置",
icon: "Setting",
},
},
{
path: "source",
component: TableLayout,
meta: {
title: "资源管理",
icon: "Refresh",
},
},
],
},
];
export default routes;

60
tools.ts Normal file
View File

@ -0,0 +1,60 @@
import type { Form } from 'lib/type/TableData'
export function formatConfig(config: Array<any>) {
if (typeof config === 'object') {
config = JSON.parse(JSON.stringify(config))
}
config?.forEach((item: any) => {
let defaultValue: any = ''
if (item.type === 'text') {
item.type = 'input'
item.rows = 5
defaultValue = item.value || ''
} else if (item.type === 'radio') {
item.type = 'select'
item.items = item.option.map((item: any) => {
return {
key: item.value,
name: item.name
}
})
delete item.option
defaultValue = item.value || ''
} else if (item.type === 'checkbox') {
item.multiple = true
item.type = 'select'
item.items = item.option.map((item: any) => {
return {
key: item.value,
name: item.name
}
})
delete item.option
defaultValue = item.value.split(',')
if (defaultValue.length === 1 && defaultValue[0] === '') {
defaultValue = []
}
} else if (item.type === 'switch') {
item.openValue = true
item.closeValue = false
defaultValue = !!item.value
} else if (item.type === 'json') {
item.type = 'jsonInput'
defaultValue = item.value || ''
} else {
defaultValue = item.value || ''
}
item.getValue = () => defaultValue
delete item.value
})
return config
}
export function updateFormData(form: Form, data: Array<any>) {
while (form.data.length > 0) {
form.data.pop()
}
for (const d of data) {
form.data.push(d)
}
}

37
views/ConfigView.scss Normal file
View File

@ -0,0 +1,37 @@
.upload-flie {
.el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 64px;
height: 64px;
text-align: center;
}
.uploaded-img{
width: 64px;
height: 64px;
display: block;
}
}
.tabs {
width: calc(100% - 60px);
margin: 10px auto 0 30px;
:deep(.el-tabs__header) {
margin: 0;
}
}

201
views/ConfigView.tsx Normal file
View File

@ -0,0 +1,201 @@
import { reactive } from 'vue'
import './ConfigView.scss'
import { ElMessage } from 'element-plus'
import login from 'src/api/login'
import { indexStore } from 'lib/stores'
export default {
setup() {
const store = indexStore()
const bean = reactive({
resetPasswordDialogShow: false,
resetPasswordInput: '',
resetPasswordInputOld: '',
resetPasswordInputTwo: '',
editorConfigDialogShow: false,
editorConfigInfo: {
oa_appid: '',
oa_secret: '',
link_prev:'',
show_conn:"false",
show_seal:"false",
// service_phone: '',
// service_qrcode: {
// url: ''
// },
service_weixin:"",
robot_key:'',
// service_type: '1',
front_host: '',
back_host: '',
privacy: '',
user: ''
}
})
const methods = reactive({
handleEditorConfigSubmit() {
login.editorConfig(bean.editorConfigInfo).then((res: any) => {
if (res.code == 0) {
ElMessage.success('配置修改成功')
} else {
ElMessage.success('配置修改失败')
}
methods.updateConfig()
bean.editorConfigDialogShow = false
})
},
handleResetPasswordSubmit() {
if (bean.resetPasswordInputOld === '') {
ElMessage.error('旧密码不能为空')
return
}
if (bean.resetPasswordInput === '') {
ElMessage.error('密码不能为空')
return
}
if (bean.resetPasswordInput !== bean.resetPasswordInputTwo) {
ElMessage.error('两次输入的密码不一致')
return
}
login
.resetPassWord({
old_password: bean.resetPasswordInputOld,
password: bean.resetPasswordInput
})
.then((res: any) => {
if (res.code === 0) {
ElMessage.success('密码修改成功')
setTimeout(() => {
store.logout()
}, 1000)
} else {
ElMessage.error(res.message)
}
})
},
updateConfig() {
login.getConfig().then((res) => {
bean.editorConfigInfo = res.data
})
}
})
methods.updateConfig()
return () => {
return (
<div class="page" style="height: 100%;display: flex;flex-direction: column;">
{/* <el-tabs vModel={state.activeName} type="card" class="tabs">
<el-tab-pane label="修改密码" name="1">
<el-form label-width="80">
<el-form-item label="旧密码">
<el-input type="password" vModel={bean.resetPasswordInputOld}></el-input>
</el-form-item>
<el-form-item label="新密码">
<el-input type="password" vModel={bean.resetPasswordInput}></el-input>
</el-form-item>
<el-form-item label="再次输入">
<el-input type="password" vModel={bean.resetPasswordInputTwo}></el-input>
</el-form-item>
<el-form-item>
<el-button
type="primary"
onClick={() => {
methods.handleResetPasswordSubmit()
}}
>
</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="修改配置" name="2"> */}
<el-form label-width="100" class="tabs">
<el-form-item label="APPID">
<el-input vModel={bean.editorConfigInfo.oa_appid}></el-input>
</el-form-item>
<el-form-item label="SECRET">
<el-input vModel={bean.editorConfigInfo.oa_secret}></el-input>
</el-form-item>
<el-form-item label="客服链接">
<el-input vModel={bean.editorConfigInfo.service_weixin}></el-input>
</el-form-item>
{/* <el-form-item label="">
<el-select vModel={bean.editorConfigInfo.service_type}>
<el-option label="手机" value="1"></el-option>
<el-option label="二维码" value="2"></el-option>
</el-select>
</el-form-item>
{bean.editorConfigInfo.service_type == '1' ? (
<el-form-item label="电话号码">
<el-input vModel={bean.editorConfigInfo.service_phone}></el-input>
</el-form-item>
) : (
<el-form-item label="二维码">
<el-upload
class="upload-flie"
action="/corpapi/corp/config/upload"
headers={{
'x-token': store.$state.token,
'x-host': window.location.host
// 'x-host': 'a1.batiao8.com'
}}
show-file-list={false}
onSuccess={(response: any) => {
bean.editorConfigInfo.service_qrcode = response.data
}}
>
{bean.editorConfigInfo.service_qrcode ? (
<img class="uploaded-img" src={bean.editorConfigInfo.service_qrcode.url} />
) : (
<el-icon class="avatar-uploader-icon">
<Plus />
</el-icon>
)}
</el-upload>
</el-form-item>
)} */}
{/* front_host:"",
back_host:"",
privacy:"",
user:"" */}
<el-form-item label="前端域名">
<el-input vModel={bean.editorConfigInfo.front_host}></el-input>
</el-form-item>
<el-form-item label="后端域名">
<el-input vModel={bean.editorConfigInfo.back_host}></el-input>
</el-form-item>
<el-form-item label="机器人配置">
<el-input vModel={bean.editorConfigInfo.robot_key}></el-input>
</el-form-item>
<el-form-item label="隐私政策">
<el-input type="textarea" vModel={bean.editorConfigInfo.privacy}></el-input>
</el-form-item>
<el-form-item label="用户协议">
<el-input type="textarea" vModel={bean.editorConfigInfo.user}></el-input>
</el-form-item>
<el-form-item label="链接前缀">
<el-input type="textarea" vModel={bean.editorConfigInfo.link_prev}></el-input>
</el-form-item>
<el-form-item label="查看联络人">
<el-switch vModel={bean.editorConfigInfo.show_conn} activeValue={'true'} inactiveValue={'false'}></el-switch>
</el-form-item>
<el-form-item label="打印公章">
<el-switch vModel={bean.editorConfigInfo.show_seal} activeValue={'true'} inactiveValue={'false'}></el-switch>
</el-form-item>
<el-form-item>
<el-button
type="primary"
onClick={() => {
methods.handleEditorConfigSubmit()
}}
>
</el-button>
</el-form-item>
</el-form>
{/* </el-tab-pane>
</el-tabs> */}
</div>
)
}
}
}

5
views/HomeView.vue Normal file
View File

@ -0,0 +1,5 @@
<template>
<div>
首页
</div>
</template>

197
views/LoginPage.vue Normal file
View File

@ -0,0 +1,197 @@
<template>
<div
class="page"
style="
background: url('src/pub/login.jpg');
background-repeat: no-repeat;
background-size: 100% 100%;
"
>
<div class="login-box flex-col">
<div class="title">{{ corpName }}</div>
<div class="input-box flex-row">
<label class="input-label">账号</label>
<ElInput class="input" v-model="username"></ElInput>
</div>
<div class="input-box flex-row">
<label class="input-label">密码</label>
<ElInput class="input" v-model="password" type="password" show-password></ElInput>
</div>
<!-- <div v-if="!hideCorpList" class="input-box flex-row">
<label class="input-label">企业</label>
<ElSelect
placeholder="请选择要登陆的企业"
class="input"
v-model="corp"
type="password"
show-password
>
<ElOption
v-for="(item, index) in corpList"
:key="index"
:label="(item as any).name"
:value="(item as any).id"
>
</ElOption>
</ElSelect>
</div> -->
<ElButton type="primary" class="login" @click="login">登录</ElButton>
</div>
</div>
</template>
<script lang="ts">
import { getConfig } from 'lib/config'
import { indexStore } from 'lib/stores'
import message from 'lib/utils/message'
import login from 'src/api/login'
// import Request from 'lib/utils/requests'
export default {
data() {
return {
username: '',
password: '',
corp: '',
baseUrl: getConfig('baseUrl'),
corpList: [],
corpName: ''
// hideCorpList: true
}
},
mounted() {
const store = indexStore()
store.logout(true)
store.$patch({
headers: {
'x-host': import.meta.env.VITE_PUBLIC_ENV == 'dev' ? 'a1.batiao8.com' : window.location.host
}
})
// Request({
// url: '/corp/available/corp',
// method: 'get'
// }).then((res: any) => {
// this.corpList = res.data
// const params = this.getHashParams()
// if (params.corp_id) {
// this.corp = params.corp_id
// this.hideCorpList = true
// }
// })
//
this.corpName = '银泰官网后台'
store.$patch({
title: this.corpName
})
window.onkeydown = (e) => {
if (e.key == 'Enter') {
this.login()
}
}
},
unmounted() {
window.onkeydown = null
},
methods: {
getHashParams() {
// hash'#'
let hash = window.location.hash
.replace(window.location.hash.split('?')[0], '')
.replace('?', '')
// hash
let params: any = {}
let vars = hash.split('&')
for (let i = 0; i < vars.length; i++) {
let pair = vars[i].split('=')
params[pair[0]] = decodeURIComponent(pair[1])
}
return params
},
login() {
if (this.username == '') {
message.error('请输入用户名')
return
}
if (this.password == '') {
message.error('请输入密码')
return
}
// if (!this.corp) {
// message.error('')
// return
// }
// const corp: any = this.corpList.find((item: any) => item.id == this.corp)
const store = indexStore()
store.$patch({
baseUrl: this.baseUrl
})
store
.login({
username: this.username,
password: this.password
})
.then((res: any) => {
store.$patch({
token: res.data.token,
nickname: this.username,
username: this.username,
password: this.password,
// tableData: res.data.table,
// menus: res.data.menus,
kwargs: res.data
})
})
.finally(() => {
this.$router.push('/')
// setTimeout(() => {
// location.reload()
// }, 1000)
})
}
}
}
</script>
<style lang="scss" scoped>
.page {
// background-image: url("src/pub/login.jpg");
.login-box {
//height: 400px;
width: 400px;
// border: solid;
background-color: #fff;
margin: auto;
border-radius: 10px;
box-shadow: 1px 1px 10px 2px rgb(69, 82, 141);
.title {
margin: 30px auto 20px auto;
font-size: 30px;
font-weight: bold;
color: rgb(26, 36, 83);
}
.input-box {
margin: 10px auto;
width: 300px;
.input-label {
align-self: center;
width: 40px;
}
.input {
width: 260px;
}
}
.login {
margin: 30px auto;
width: 300px;
height: 40px;
}
}
}
</style>

167
views/admin/list.tsx Normal file
View File

@ -0,0 +1,167 @@
import type { TableData } from 'lib/type/TableData'
const data: TableData = {
async fetchFun(self, data) {
const res = await self.api?.getDataList(data)
return res
},
addNods: [
],
launchTask: [
async (self) => {
if (self.bean) {
const res = await self.api?.getRoleList()
self.bean.roleList = res.data.items.map((item:any) => ({key: item.id, name: item.name}))
}
}
],
fliter: [
{
key: 'id',
name: 'ID',
type: 'input'
},
{
key: 'username',
name: '用户名',
type: 'input'
},
{
type: 'dialogForm',
key: 'create',
form: {
title: '新建',
data: [
{
key: 'username',
name: '用户名',
type: 'input',
must: true
},
{
key: 'account',
name: '账号',
type: 'input',
must: true
},
{
key: 'password',
name: '密码',
type: 'input',
must: true
},
{
key: 'role',
name: '角色',
type: 'select',
getItems: (self) => {
return self.bean ? self.bean.roleList : []
},
must: true
},
],
subFun(self, data) {
return self.api?.addData(data)
}
}
}
],
tableColumns: [
{
key: 'id',
name: 'ID',
width: '80px',
showJson: '*'
},
{
key: 'username',
name: '用户名',
width: '200px'
},
{
key: 'account',
name: '账号',
width: '200px'
},
{
key: 'role_name',
name: '角色',
width: '200px'
},
{
key: 'status',
name: '状态',
width: '100px',
editor: {
type: 'switch',
openValue: 1,
closeValue: 2,
subFun(self, data) {
return self.api?.updateData({id: String(data.id), status: String(data.status)})
},
}
},
{
key: 'create_time',
name: '创建时间',
},
{
key: 'table_tools',
name: '操作',
buttons: [
{
type: 'dialogForm',
key: 'update',
form: {
title: '编辑',
type: 'warning',
primary: 'id',
data: [
{
key: 'username',
name: '用户名',
type: 'input',
must: true
},
{
key: 'role_id',
name: '角色',
type: 'select',
getItems: (self) => {
return self.bean ? self.bean.roleList : []
},
must: true
},
{
key: 'status',
name: '状态',
type: 'switch',
openValue: 1,
closeValue: 2,
tips: '启用/禁用',
must: true
},
],
subFun(self, data) {
return self.api?.updateData({...data, id: String(data.id), role_id: String(data.role_id), status: String(data.status)})
}
}
},
{
type: 'popoverConfirm',
key: 'delete',
confirm: {
title: '删除',
primary: 'id',
subFun(self, data) {
return self.api?.deleteData(data)
}
}
}
]
}
]
}
export default data

139
views/admin/role.tsx Normal file
View File

@ -0,0 +1,139 @@
import type { TableData } from 'lib/type/TableData'
const data: TableData = {
async fetchFun(self, data) {
const res = await self.api?.getDataList(data)
return res
},
addNods: [
],
launchTask: [
async (self: any) => {
if (self.bean) {
const res = await self.api?.getResources()
self.bean.resources = res.data.items.map((item:any) => {
const type = item.type === 'page' ? '页面' : '接口'
return {
key: item.id,
name: `${type}${item.name}`
}
})
}
}
],
fliter: [
{
key: 'id',
name: 'ID',
type: 'input'
},
{
key: 'username',
name: '用户名',
type: 'input'
},
{
type: 'dialogForm',
key: 'create',
form: {
title: '新建',
data: [
{
key: 'name',
name: '角色',
type: 'input',
must: true
},
{
key: 'resource',
name: '权限',
type: 'select',
getItems: (self: any) => {
return self.bean ? self.bean.resources : []
},
multiple: true,
must: true
},
],
subFun(self, data) {
const params = {
...data,
resource: data.resource.map((i:number) => String(i))
}
return self.api?.addData(params)
}
}
}
],
tableColumns: [
{
key: 'id',
name: 'ID',
width: '80px',
showJson: '*'
},
{
key: 'name',
name: '角色',
width: '200px'
},
{
key: 'desc',
name: '描述',
},
{
key: 'create_time',
name: '创建时间',
},
{
key: 'table_tools',
name: '操作',
buttons: [
{
type: 'dialogForm',
key: 'update',
form: {
title: '编辑',
type: 'warning',
primary: 'id',
data: [
{
key: 'name',
name: '角色',
type: 'input',
must: true
},
{
key: 'resource',
name: '权限',
type: 'select',
getItems: (self: any) => {
return self.bean ? self.bean.resources : []
},
multiple: true,
must: true
},
],
subFun(self, data) {
return self.api?.updateData({...data, id: String(data.id)})
}
}
},
{
type: 'popoverConfirm',
key: 'delete',
confirm: {
title: '删除',
primary: 'id',
subFun(self, data) {
return self.api?.deleteData(data)
}
}
}
]
}
]
}
export default data

20
views/config/index.less Normal file
View File

@ -0,0 +1,20 @@
.category-row,
.active-category-row {
.cell {
display: flex;
align-items: center;
}
&.el-table__row--level-0 .el-tooltip__trigger>span {
margin: 0 !important;
text-align: left;
}
&.el-table__row--level-0 {
font-weight: 600;
}
}
.active-category-row {
background: greenyellow !important;
}

273
views/config/list.tsx Normal file
View File

@ -0,0 +1,273 @@
import type { TableData } from 'lib/type/TableData'
import './index.less'
import JsonFormDialog from 'src/components/JsonFormDialog.vue'
const data: TableData = {
rowKey: 'id',
defaultExpandAll: true,
sizeOption: [1000, 100, 500, 2000],
fetchFun(self, data) {
return self.api?.getDataList(data).then((res: any) => {
const items = res.data.items || []
const backendItem = items.find((item:any) => item.tags.includes('backend'))
if(backendItem) {
try {
(self.bean as any).fieldMap = JSON.parse(backendItem.content || '{}')
} catch(err) {
console.log(err)
}
}
const flatData = items.map((item: any) => ({
...item,
key: item.id,
id: item.id,
pid: String(item.pid ?? 0),
color: 'category'
}))
res.data.items = self.methods?.getTree(flatData) ?? flatData
return res
})
},
methods: {
getTree(arr: any[]) {
const map: any = {}
const roots: any[] = []
arr.forEach((item: any) => {
map[item.key] = { ...item, children: [] }
})
arr.forEach((item: any) => {
const node = map[item.key]
if (item.pid === 0 || item.pid === '0') {
roots.push(node)
} else {
if (map[item.pid]) {
map[item.pid].children.push(node)
} else {
roots.push(node)
}
}
})
return roots
}
},
addNods: [
(self: any) => {
return self.bean && <JsonFormDialog
show={self.bean.showJsonFormDialog}
title={self.bean?.currentRow?.name || '配置'}
data={self.bean.jsonFormData}
fieldMap={self.bean.fieldMap}
onClose={() => {
self.bean.showJsonFormDialog = false
}}
onSubmit={async (data: any) => {
await self.api?.updateData({
id: String(self.bean.currentRow.id),
content: JSON.stringify(data),
})
self.bean.showJsonFormDialog = false
self.methods.fetchData()
}}
/>
}
],
launchTask: [
async (self: any) => {
if (self.bean) {
self.bean.showJsonFormDialog = false
self.bean.currentRow = null
self.bean.jsonFormData = {}
// self.bean.fieldMap = {}
}
}
],
fliter: [
{
key: 'id',
name: 'ID',
type: 'input'
},
{
key: 'name',
name: '配置名',
type: 'input'
},
{
type: 'dialogForm',
key: 'create',
form: {
title: '新建',
data: [
{
key: 'pid',
name: '父级',
type: 'input',
},
{
key: 'name',
name: '配置名',
type: 'input',
must: true
},
{
key: 'page',
name: '页面路径',
type: 'input',
must: true
},
{
key: 'extra',
hide: true,
name: '配置内容',
type: 'input',
rows: 4
},
],
subFun(self, data) {
return self.api?.addData(data)
}
}
}
],
tableColumns: [
{
key: 'id',
name: 'ID',
width: '80px',
showJson: '*'
},
{
key: 'pid',
name: 'PID',
width: '80px',
showJson: '*'
},
{
key: 'name',
name: '配置名',
width: '150px'
},
{
key: 'page',
name: '页面路径',
width: '150px'
},
{
key: 'content',
name: 'JSON配置',
width: '200px',
editor: {
type: 'json',
subFun(self, data) {
return self.api?.updateData({...data, id: String(data.id)})
}
}
},
{
key: 'content',
name: '配置',
width: '100px',
renderBodyCell({self, row}:any) {
if(row.tags.includes('backend')) {
return <span></span>
}
return (
<el-button size="small" type="primary"
onClick={() => {
try {
self.bean.jsonFormData = JSON.parse(row.content || '{}')
} catch {
self.bean.jsonFormData = {}
}
self.bean.currentRow = row
self.bean.showJsonFormDialog = true
}}
></el-button>
)
},
},
{
key: 'create_time',
name: '创建时间',
},
{
key: 'weight',
name: '权重',
width: '100px',
editor: {
type: 'input',
subFun(self, data, row) {
return self.api?.updateData({...data, id: String(data.id), pid: String(row.pid)})
}
}
},
{
key: 'tags',
name: '标签',
editor: {
type: 'input',
subFun(self, data, row) {
return self.api?.updateData({...data, id: String(data.id), pid: String(row.pid)})
}
}
},
{
key: 'table_tools',
name: '操作',
buttons: [
{
type: 'dialogForm',
key: 'update',
form: {
title: '编辑',
type: 'warning',
primary: 'id',
data: [
{
key: 'pid',
name: '父级',
type: 'input',
},
{
key: 'name',
name: '配置名',
type: 'input',
must: true
},
{
key: 'page',
name: '页面路径',
type: 'input',
must: true
},
{
key: 'content',
hide: true,
name: '配置内容',
type: 'input',
rows: 4
},
],
subFun(self, data) {
return self.api?.updateData({...data, id: String(data.id)})
}
}
},
{
type: 'popoverConfirm',
key: 'delete',
confirm: {
title: '删除',
primary: 'id',
subFun(self, data) {
return self.api?.deleteData(data)
}
}
}
]
}
]
}
export default data

158
views/config/source.tsx Normal file
View File

@ -0,0 +1,158 @@
import type { TableData } from 'lib/type/TableData'
const data: TableData = {
async fetchFun(self, data) {
const res = await self.api?.getDataList(data)
return res
},
addNods: [
],
launchTask: [
],
fliter: [
{
key: 'id',
name: 'ID',
type: 'input'
},
{
key: 'name',
name: '名称',
type: 'input'
},
{
key: 'type',
name: '类型',
type: 'select',
items: [
{ key: 'page',name: '页面' },
{ key: 'api',name: 'API' },
],
value: 'page'
},
{
key: 'path',
name: '路径',
type: 'input',
},
{
type: 'dialogForm',
key: 'create',
form: {
title: '新建',
data: [
{
key: 'name',
name: '名称',
type: 'input',
must: true
},
{
key: 'type',
name: '类型',
type: 'select',
items: [
{ key: 'page',name: '页面' },
{ key: 'api',name: 'API' },
],
value: 'page'
},
{
key: 'path',
name: '路径',
type: 'input',
must: true
},
],
subFun(self, data) {
return self.api?.addData(data)
}
}
}
],
tableColumns: [
{
key: 'id',
name: 'ID',
width: '80px',
showJson: '*'
},
{
key: 'name',
name: '名称',
width: '200px'
},
{
key: 'type',
name: '类型',
},
{
key: 'path',
name: '路径',
},
{
key: 'method',
name: '方法',
},
{
key: 'create_time',
name: '创建时间',
},
{
key: 'table_tools',
name: '操作',
buttons: [
{
type: 'dialogForm',
key: 'update',
form: {
title: '编辑',
type: 'warning',
primary: 'id',
data: [
{
key: 'name',
name: '名称',
type: 'input',
must: true
},
{
key: 'type',
name: '类型',
type: 'select',
items: [
{ key: 'page',name: '页面' },
{ key: 'api',name: 'API' },
],
value: 'page'
},
{
key: 'path',
name: '路径',
type: 'input',
must: true
},
],
subFun(self, data) {
return self.api?.updateData({...data, id: String(data.id)})
}
}
},
{
type: 'popoverConfirm',
key: 'delete',
confirm: {
title: '删除',
primary: 'id',
subFun(self, data) {
return self.api?.deleteData(data)
}
}
}
]
}
]
}
export default data

6
views/default.tsx Normal file
View File

@ -0,0 +1,6 @@
import type { TableData } from "lib/type/TableData"
const data:TableData = {
}
export default data

212
views/document/list.tsx Normal file
View File

@ -0,0 +1,212 @@
import type { TableData } from 'lib/type/TableData'
import In18FormDialog from '../../components/In18FormDialog.vue'
import { LOCALES } from '../../data/const'
import { categoryTypes } from '../../data/const'
import utils from 'lib/utils'
let types: any[] = []
const pathname = utils.getPathName()
types = categoryTypes.filter(item => pathname.includes(item.path))
const data: TableData = {
async fetchFun(self, data) {
const res = await self.api?.getDataList(data)
return res
},
addNods: [
(self: any) => {
return (
self.bean &&
<In18FormDialog
show={self.bean.showEditorDialog}
id={self.bean.id}
form={self.bean.form}
locales={self.bean.locales}
onClose={() => {
self.bean.showEditorDialog = false;
self.bean.id = null;
}}
onSubmit={async (data) => {
const params = {
...data,
id: data.id ? String(data.id) : '',
category_id: data.category_id ? String(data.category_id) : '',
}
if(data.id) {
await self.api?.updateData(params)
} else {
await self.api?.addData(params)
}
self.bean.showEditorDialog = false;
self.bean.id = null;
self.methods.fetchData()
}}
translateApi={self.api?.translate}
detailApi={async (id: number) => {
const res = await self.api?.getDetail(id);
const data = res.data;
Object.keys(data).forEach((langKey) => {
const langData = data[langKey];
Object.keys(langData).forEach((fieldKey) => {
const field = self.bean.form.find((f:any) => f.key === fieldKey);
if(fieldKey === 'name') {
data[langKey]['file_name'] = langData[fieldKey]
}
if (!field) return;
data[langKey][fieldKey] = langData[fieldKey];
})
})
return data
}}
uploadFun={self.api?.upload}
/>
)
}
],
launchTask: [
async (self: any) => {
if (self.bean) {
self.bean.id = null;
self.bean.form = [
{
name: '分类',
key: 'category_id',
type: 'select',
getItems: () => self.bean.docTypes,
must: true,
shouldTranslate: false,
},
{
name: '文件路径',
key: 'path',
type: 'upload:file',
accept: '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx',
must: true,
shouldTranslate: false,
},
{
name: '文件名',
key: 'file_name',
type: 'input',
value: '',
must: true,
shouldTranslate: true,
},
];
self.bean.locales = LOCALES
}
},
async (self: any) => {
if (self.bean) {
const types = ['file'] as const
const results = await Promise.all(
types.map((type) =>
self.api?.getCategoryList({ page: 1, size: 1000, type })
)
)
const toOptions = (items: any[]) => items.map((item: any) => ({ key: item.id, name: item.name }));
[self.bean.docTypes] = results.map(
(res) => toOptions(res?.data?.items ?? [])
)
console.log('docTypes', self.bean.docTypes)
}
}
],
fliter: [
{
key: 'id',
name: 'ID',
type: 'input'
},
{
key: 'lang',
name: '语言',
type: 'select',
value: 'ZH',
getItems: (self) => {
return self.bean ? self.bean.locales : []
}
},
{
key: 'category_id',
name: '分类',
type: 'select',
value: '',
getItems: (self: any) => {
return self.bean ? self.bean.docTypes : []
},
},
{
key: 'file_name',
name: '文件名',
type: 'input'
},
{
type: 'onlyFun',
onlyFun: {
title: '新建',
type: ' ' as any,
fun(self) {
if(self.bean) {
self.bean.id = null;
self.bean.showEditorDialog = true;
}
}
}
},
],
tableColumns: [
{
key: 'id',
name: 'ID',
width: '80px',
showJson: '*'
},
{
key: 'name',
name: '文件名',
width: '200px'
},
{
key: 'path',
name: '文件路径',
},
{
key: 'create_time',
name: '创建时间',
},
{
key: 'table_tools',
name: '操作',
buttons: [
{
type: 'onlyFun',
key: 'update',
onlyFun: {
title: '编辑',
type: ' ' as any,
fun(self, row) {
if(self.bean) {
self.bean.id = row.id;
self.bean.showEditorDialog = true;
}
}
}
},
{
type: 'popoverConfirm',
key: 'delete',
confirm: {
title: '删除',
primary: 'id',
subFun(self, data) {
return self.api?.deleteData(data)
}
}
}
]
}
]
}
export default data

View File

@ -0,0 +1,3 @@
import data from '../history/typesManage'
export default data

212
views/history/list.tsx Normal file
View File

@ -0,0 +1,212 @@
import type { TableData } from 'lib/type/TableData'
import In18FormDialog from '../../components/In18FormDialog.vue'
import { LOCALES } from '../../data/const'
import { categoryTypes } from '../../data/const'
import utils from 'lib/utils'
let types: any[] = []
const pathname = utils.getPathName()
types = categoryTypes.filter(item => pathname.includes(item.path))
const data: TableData = {
async fetchFun(self, data) {
const res = await self.api?.getDataList(data)
return res
},
addNods: [
(self: any) => {
return (
self.bean &&
<In18FormDialog
show={self.bean.showEditorDialog}
id={self.bean.id}
form={self.bean.form}
locales={self.bean.locales}
onClose={() => {
self.bean.showEditorDialog = false;
self.bean.id = null;
}}
onSubmit={async (data) => {
await self.api?.addData(data)
self.bean.showEditorDialog = false;
self.bean.id = null;
self.methods.fetchData()
}}
translateApi={self.api?.translate}
detailApi={async (id: number) => {
const res = await self.api?.getDetail(id);
const data = res.data;
Object.keys(data).forEach((langKey) => {
const langData = data[langKey];
Object.keys(langData).forEach((fieldKey) => {
const field = self.bean.form.find((f:any) => f.key === fieldKey);
if (!field) return;
data[langKey][fieldKey] = langData[fieldKey];
})
})
return data
}}
/>
)
}
],
launchTask: [
async (self: any) => {
if (self.bean) {
self.bean.id = null;
self.bean.form = [
{
name: '分类',
key: 'category_id',
type: 'select',
must: true,
getItems: () => {
return self.bean ? self.bean.processTypes : []
},
shouldTranslate: false,
},
{
name: '标题',
key: 'title',
type: 'input',
value: '',
must: true,
shouldTranslate: true,
},
{
name: '内容',
key: 'content',
type: 'textarea',
value: '',
must: true,
shouldTranslate: true,
},
];
self.bean.locales = LOCALES
}
},
async (self: any) => {
if (self.bean) {
const types = ['process'] as const
const results = await Promise.all(
types.map((type) =>
self.api?.getCategoryList({ page: 1, size: 1000, type })
)
)
const toOptions = (items: any[]) => items.map((item: any) => ({ key: item.id, name: item.name }));
[self.bean.processTypes] = results.map(
(res) => toOptions(res?.data?.items ?? [])
)
console.log('processTypes', self.bean.processTypes)
}
}
],
fliter: [
{
key: 'id',
name: 'ID',
type: 'input'
},
{
key: 'lang',
name: '语言',
type: 'select',
value: 'ZH',
getItems: (self) => {
return self.bean ? self.bean.locales : []
}
},
{
key: 'category_id',
name: '分类',
type: 'select',
value: '',
getItems: (self: any) => {
return self.bean ? self.bean.processTypes : []
},
},
{
key: 'title',
name: '标题',
type: 'input'
},
{
type: 'onlyFun',
onlyFun: {
title: '新建',
type: ' ' as any,
fun(self) {
if(self.bean) {
self.bean.id = null;
self.bean.showEditorDialog = true;
}
}
}
},
],
tableColumns: [
{
key: 'id',
name: 'ID',
width: '80px',
showJson: '*'
},
{
key: 'lang',
name: '语言',
width: '80px',
},
{
key: 'category_name',
name: '分类',
width: '200px',
},
{
key: 'title',
name: '标题',
width: '200px',
},
{
key: 'content',
name: '内容',
},
{
key: 'create_time',
name: '创建时间',
width: '150px'
},
{
key: 'table_tools',
name: '操作',
buttons: [
{
type: 'onlyFun',
key: 'update',
onlyFun: {
title: '编辑',
type: ' ' as any,
fun(self, row) {
if(self.bean) {
self.bean.id = row.id;
self.bean.showEditorDialog = true;
}
}
}
},
{
type: 'popoverConfirm',
key: 'delete',
confirm: {
title: '删除',
primary: 'id',
subFun(self, data) {
return self.api?.deleteData(data)
}
}
}
]
}
]
}
export default data

View File

@ -0,0 +1,140 @@
import type { TableData } from 'lib/type/TableData'
import { categoryTypes } from '../../data/const'
import utils from 'lib/utils'
let types: any[] = []
const pathname = utils.getPathName()
types = categoryTypes.filter(item => pathname.includes(item.path))
const data: TableData = {
async fetchFun(self, data) {
const res = await self.api?.getDataList(data)
return res
},
addNods: [
],
launchTask: [
async (self) => {
const pathname = utils.getPathName()
types = categoryTypes.filter(item => pathname.includes(item.path))
}
],
fliter: [
{
key: 'id',
name: 'ID',
type: 'input'
},
{
key: 'type',
name: '类型',
type: 'select',
getValue: () => types[0] ? types[0].key : '',
getItems: () => types,
},
{
key: 'name',
name: '分类名',
type: 'input'
},
{
type: 'dialogForm',
key: 'create',
form: {
title: '新建',
data: [
{
key: 'pid',
name: '父ID',
type: 'input',
},
{
key: 'name',
name: '分类名',
type: 'input',
must: true
},
{
key: 'type',
name: '类型',
type: 'select',
getItems: () => types,
must: true
},
],
subFun(self, data) {
return self.api?.addData(data)
}
}
}
],
tableColumns: [
{
key: 'id',
name: 'ID',
width: '80px',
showJson: '*'
},
{
key: 'pid',
name: 'PID',
width: '80px',
showJson: '*'
},
{
key: 'name',
name: '分类名',
},
{
key: 'table_tools',
name: '操作',
buttons: [
{
type: 'dialogForm',
key: 'update',
form: {
title: '编辑',
type: 'warning',
primary: 'id',
data: [
{
key: 'pid',
name: '父ID',
type: 'input',
},
{
key: 'name',
name: '分类名',
type: 'input',
must: true
},
// {
// key: 'type',
// name: '类型',
// type: 'select',
// getItems: () => types,
// must: true
// },
],
subFun(self, data) {
return self.api?.updateData({ ...data, id: String(data.id), pid: String(data.pid) })
}
}
},
{
type: 'popoverConfirm',
key: 'delete',
confirm: {
title: '删除',
primary: 'id',
subFun(self, data) {
return self.api?.deleteData(data)
}
}
}
]
}
]
}
export default data

317
views/jobs/list.tsx Normal file
View File

@ -0,0 +1,317 @@
import type { TableData } from 'lib/type/TableData'
import In18FormDialog from '../../components/In18FormDialog.vue'
import { LOCALES } from '../../data/const'
const data: TableData = {
async fetchFun(self, data) {
const res = await self.api?.getDataList(data)
return res
},
addNods: [
(self: any) => {
return (
self.bean &&
<In18FormDialog
show={self.bean.showEditorDialog}
id={self.bean.id}
form={self.bean.form}
locales={self.bean.locales}
onClose={() => {
self.bean.showEditorDialog = false;
self.bean.id = null;
}}
onSubmit={async (data) => {
const params = {
...data,
id: data.id ? String(data.id) : undefined,
job_area: data.job_area ? String(data.job_area) : undefined,
job_unit: data.job_unit ? String(data.job_unit) : undefined,
job_type: data.job_type ? String(data.job_type) : undefined,
recruit_count: data.recruit_count ? String(data.recruit_count) : undefined,
}
if(data.id) {
await self.api?.updateData(params)
} else {
await self.api?.addData(params)
}
self.bean.showEditorDialog = false;
self.bean.id = null;
self.methods.fetchData()
}}
translateApi={self.api?.translate}
detailApi={async (id: number) => {
const res = await self.api?.getDetail(id);
const data = res.data;
Object.keys(data).forEach((langKey) => {
const langData = data[langKey];
Object.keys(langData).forEach((fieldKey) => {
const field = self.bean.form.find((f:any) => f.key === fieldKey);
if (fieldKey === 'title') {
data[langKey]['name'] = langData[fieldKey];
}
if(fieldKey === 'category') {
data[langKey]['job_type'] = langData[fieldKey].job_type;
data[langKey]['job_area'] = langData[fieldKey].job_area;
data[langKey]['job_unit'] = langData[fieldKey].job_unit;
}
if (!field) return;
data[langKey][fieldKey] = langData[fieldKey];
})
})
console.log('data', data)
return data
}}
/>
)
}
],
launchTask: [
async (self: any) => {
if (self.bean) {
self.bean.id = null;
console.log('self.bean.jobTypes', self.bean.jobTypes)
self.bean.form = [
{
name: '职业类别',
key: 'job_type',
type: 'select',
value: '',
getItems: () => self.bean.jobTypes,
must: true,
shouldTranslate: false,
},
{
name: '业务领域',
key: 'job_area',
type: 'select',
getItems: () => self.bean.jobAreas,
value: '',
must: true,
shouldTranslate: false,
},
{
name: '所属板块',
key: 'job_unit',
type: 'select',
getItems: () => self.bean.jobUnits,
value: '',
must: true,
shouldTranslate: false,
},
{
name: '招聘人数',
key: 'recruit_count',
type: 'input',
value: '',
must: true,
shouldTranslate: false,
},
{
name: '城市',
key: 'city',
type: 'input',
value: '',
must: true,
shouldTranslate: false,
},
{
name: '岗位名称',
key: 'name',
type: 'input',
value: '',
must: true,
shouldTranslate: true,
},
{
name: '岗位描述',
key: 'description',
type: 'textarea',
value: '',
must: true,
shouldTranslate: true,
},
{
name: '任职需求',
key: 'requirement',
type: 'textarea',
value: '',
must: true,
shouldTranslate: true,
},
{
name: '联系方式',
key: 'contract_info',
type: 'input',
value: '',
must: true,
shouldTranslate: true,
},
];
self.bean.locales = LOCALES
}
},
async (self: any) => {
if (self.bean) {
const types = ['job_type', 'job_area', 'job_unit'] as const
const results = await Promise.all(
types.map((type) =>
self.api?.getCategoryList({ page: 1, size: 1000, type })
)
)
const toOptions = (items: any[]) => items.map((item: any) => ({ key: item.id, name: item.name }));
[self.bean.jobTypes, self.bean.jobAreas, self.bean.jobUnits] = results.map(
(res) => toOptions(res?.data?.items ?? [])
)
}
}
],
fliter: [
{
key: 'id',
name: 'ID',
type: 'input'
},
{
key: 'lang',
name: '语言',
type: 'select',
value: 'ZH',
getItems: (self) => {
return self.bean ? self.bean.locales : []
}
},
{
key: 'title',
name: '职位名称',
type: 'input',
},
{
key: 'job_type',
name: '职业类别',
type: 'select',
getItems: (self) => {
return self.bean ? self.bean.jobTypes : []
}
},
{
key: 'job_area',
name: '业务领域',
type: 'select',
getItems: (self) => {
return self.bean ? self.bean.jobAreas : []
}
},
{
key: 'job_unit',
name: '所属板块',
type: 'select',
getItems: (self) => {
return self.bean ? self.bean.jobUnits : []
}
},
{
type: 'onlyFun',
onlyFun: {
title: '新建',
type: ' ' as any,
fun(self) {
if (self.bean) {
self.bean.id = null;
self.bean.showEditorDialog = true;
}
}
}
},
],
tableColumns: [
{
key: 'id',
name: 'ID',
width: '80px',
showJson: '*'
},
{
key: 'lang',
name: '语言',
width: '80px'
},
{
key: 'title',
name: '职位名称',
width: '200px'
},
{
key: 'category',
name: '职业类型 | 业务领域 | 所属板块',
renderBodyCell(data) {
let item: any = {}
try {
item = JSON.parse(data.row.category)
} catch (error) {
item = data.row.category
}
return <div style="display: flex; gap: 5px;">
<el-tag>{item.job_type_name}</el-tag>
<el-tag type="warning">{item.job_area_name}</el-tag>
<el-tag type="success">{item.job_unit_name}</el-tag>
</div>
},
},
{
key: 'description',
name: '岗位描述',
},
{
key: 'requirement',
name: '任职需求',
},
{
key: 'recruit_count',
name: '招聘人数',
width: '80px'
},
{
key: 'contract_info',
name: '联系方式',
width: '200px'
},
{
key: 'create_time',
name: '创建时间',
},
{
key: 'table_tools',
name: '操作',
buttons: [
{
type: 'onlyFun',
key: 'update',
onlyFun: {
title: '编辑',
type: ' ' as any,
fun(self, row) {
if (self.bean) {
self.bean.id = row.id;
self.bean.showEditorDialog = true;
}
}
}
},
{
type: 'popoverConfirm',
key: 'delete',
confirm: {
title: '删除',
primary: 'id',
subFun(self, data) {
return self.api?.deleteData(data)
}
}
}
]
}
]
}
export default data

View File

@ -0,0 +1,3 @@
import data from '../history/typesManage'
export default data

278
views/news/list.tsx Normal file
View File

@ -0,0 +1,278 @@
import type { TableData } from 'lib/type/TableData'
import { LOCALES } from '../../data/const'
import In18FormDialog from '../../components/In18FormDialog.vue'
const data: TableData = {
async fetchFun(self, data) {
const res = await self.api?.getDataList(data)
return res
},
addNods: [
(self: any) => {
return (
self.bean &&
<In18FormDialog
show={self.bean.showEditorDialog}
id={self.bean.id}
form={self.bean.form}
locales={self.bean.locales}
onClose={() => {
self.bean.showEditorDialog = false;
self.bean.id = null;
}}
onSubmit={async (data) => {
const params = {
...data,
id: data.id ? String(data.id) : undefined,
category_id: data.category_id ? String(data.category_id) : undefined,
}
if(data.id) {
await self.api?.updateData(params)
} else {
await self.api?.addData(params)
}
self.bean.showEditorDialog = false;
self.bean.id = null;
self.methods.fetchData()
}}
translateApi={self.api?.translate}
detailApi={async (id: number) => {
const res = await self.api?.getDetail(id);
const data = res.data;
Object.keys(data).forEach((langKey) => {
const langData = data[langKey];
Object.keys(langData).forEach((fieldKey) => {
const field = self.bean.form.find((f:any) => f.key === fieldKey);
if (fieldKey === 'covers') {
data[langKey]['cover_resource'] = langData[fieldKey];
}
if (!field) return;
data[langKey][fieldKey] = langData[fieldKey];
})
})
return data
}}
uploadFun={self.api?.upload}
/>
)
}
],
launchTask: [
async (self) => {
if (self.bean) {
self.bean.locales = LOCALES;
self.bean.id = null;
self.bean.form = [
{
name: '分类',
key: 'category_id',
type: 'select',
value: '',
getItems: () => {
return self.bean ? self.bean.newsTypes : []
},
must: true,
shouldTranslate: false,
},
{
name: '封面类型',
key: 'cover_show',
type: 'select',
items: [
{key: 'image', name: '图片'},
{key: 'video', name: '视频'},
],
value: 'image',
must: true,
shouldTranslate: false,
},
{
name: '封面',
key: 'cover_resource',
type: 'upload:image,video',
relateKey: 'cover_show',
uploadFun: self.api?.upload,
value: '',
must: true,
shouldTranslate: false,
},
{
name: '标题',
key: 'title',
type: 'input',
value: '',
must: true,
shouldTranslate: true,
},
{
name: '内容',
key: 'content',
type: 'textarea',
value: '',
must: true,
shouldTranslate: true,
},
];
}
},
async (self: any) => {
if (self.bean) {
const types = ['news'] as const
const results = await Promise.all(
types.map((type) =>
self.api?.getCategoryList({ page: 1, size: 1000, type })
)
)
const toOptions = (items: any[]) => items.map((item: any) => ({ key: item.id, name: item.name }));
[self.bean.newsTypes] = results.map(
(res) => toOptions(res?.data?.items ?? [])
)
console.log('newsTypes', self.bean.newsTypes)
}
}
],
fliter: [
{
key: 'id',
name: 'ID',
type: 'input'
},
{
key: 'lang',
name: '语言',
type: 'select',
value: 'ZH',
getItems: (self) => {
return self.bean ? self.bean.locales : []
}
},
{
key: 'category_id',
name: '分类',
type: 'select',
value: '',
getItems: (self: any) => {
return self.bean ? self.bean.newsTypes : []
},
},
{
key: 'title',
name: '标题',
type: 'input'
},
{
key: 'content',
name: '内容',
type: 'input'
},
{
key: 'publish_time',
name: '发布时间',
type: 'input'
},
{
type: 'onlyFun',
onlyFun: {
title: '新建',
type: ' ' as any,
fun(self) {
if (self.bean) {
self.bean.id = null;
self.bean.showEditorDialog = true;
}
}
}
},
],
tableColumns: [
{
key: 'id',
name: 'ID',
width: '80px',
showJson: '*'
},
{
key: 'lang',
name: '语言',
width: '80px',
},
{
key: 'category_name',
name: '分类',
width: '150px',
},
{
key: 'covers',
name: '封面',
width: '200px',
renderBodyCell(data) {
let covers = data.row.covers
try {
covers = JSON.parse(data.row.covers)
} catch (error) {
covers = data.row.covers
}
return <el-image src={covers.image} preview-src-list={[covers.image]} preview-teleported style="width: 80px; height: 100%;" />
},
},
{
key: 'covers_show',
name: '封面类型',
width: '100px',
},
{
key: 'title',
name: '标题',
width: '200px'
},
{
key: 'content',
name: '内容',
},
{
key: 'create_time',
name: '发布时间',
width: '150px'
},
{
key: 'count',
name: '阅读次数',
width: '150px'
},
{
key: 'table_tools',
name: '操作',
buttons: [
{
type: 'onlyFun',
key: 'update',
onlyFun: {
title: '编辑',
type: ' ' as any,
fun(self, row) {
if (self.bean) {
self.bean.id = row.id;
self.bean.showEditorDialog = true;
}
}
}
},
{
type: 'popoverConfirm',
key: 'delete',
confirm: {
title: '删除',
primary: 'id',
subFun(self, data) {
return self.api?.deleteData(data)
}
}
}
]
}
]
}
export default data

View File

@ -0,0 +1,3 @@
import data from '../history/typesManage'
export default data