22
CI/CD / build (push) Successful in 4m40s Details

This commit is contained in:
13121765685 2026-01-09 17:51:38 +08:00
parent b4008adb5f
commit 779b6d15dd
23 changed files with 540 additions and 378 deletions

View File

@ -29,13 +29,7 @@
移动端兼容 iOS 13+、Android 8+;桌面端兼容 Chrome 80+、Firefox 75+、Edge 80+平板端覆盖主流设备iPadOS、Android 平板)。 移动端兼容 iOS 13+、Android 8+;桌面端兼容 Chrome 80+、Firefox 75+、Edge 80+平板端覆盖主流设备iPadOS、Android 平板)。
避免使用 ES6 + 以上高级语法(或通过 Babel 转译),确保低版本浏览器兼容性。 避免使用 ES6 + 以上高级语法(或通过 Babel 转译),确保低版本浏览器兼容性。
12.设计风格 12.设计风格
- 主色调:深紫色(#6B46C1用于主要操作浅紫色#A78BFA用于强调 - 粘土拟态风格
- 辅助色:深灰色(#1F2937用于文本浅灰色#F3F4F6用于背景
- 按钮样式圆角设计8px半径微妙阴影悬停效果
- 字体排版Inter字体系列16px基础大小响应式缩放
- 布局风格基于卡片的设计统一间距8px网格系统
- 图标风格Feather图标库UI元素统一24px大小
- 动画效果平滑过渡200ms缓入缓出加载骨架屏
13.交付标准 13.交付标准
代码结构清晰,遵循 Vue3 最佳实践(如组件拆分粒度合理、逻辑与 UI 分离)。 代码结构清晰,遵循 Vue3 最佳实践(如组件拆分粒度合理、逻辑与 UI 分离)。
提供完整的多端测试报告(说明在不同设备 / 尺寸下的测试结果及适配方案)。 提供完整的多端测试报告(说明在不同设备 / 尺寸下的测试结果及适配方案)。

View File

@ -15,6 +15,7 @@
deotaland-ai-monorepo/ deotaland-ai-monorepo/
├── apps/ ├── apps/
│ └── frontend/ # 主前端应用 │ └── frontend/ # 主前端应用
│ └── FrontendDesigner/ # 管理端应用
├── packages/ # 共享包目录 ├── packages/ # 共享包目录
├── .eslintrc.base.json # 基础ESLint配置 ├── .eslintrc.base.json # 基础ESLint配置
├── .prettierrc # Prettier配置 ├── .prettierrc # Prettier配置

View File

@ -270,6 +270,7 @@ export default {
promptManagement: 'Prompt Management', promptManagement: 'Prompt Management',
productManagement: 'Product Management', productManagement: 'Product Management',
voucherManagement: 'Voucher Management', voucherManagement: 'Voucher Management',
operationLog: 'Operation Log',
logout: 'Logout', logout: 'Logout',
profile: 'Profile', profile: 'Profile',
settings: 'Settings' settings: 'Settings'
@ -929,6 +930,21 @@ export default {
hint: { hint: {
userIds: 'Multiple user IDs separated by commas, e.g.: 1,2,3' userIds: 'Multiple user IDs separated by commas, e.g.: 1,2,3'
} }
},
operationLog: {
title: 'Operation Log',
adminUserId: 'Admin User ID',
adminUsername: 'Admin Username',
operationType: 'Operation Type',
all: 'All',
resourceType: 'Resource Type',
resourceId: 'Resource ID',
description: 'Description',
ipAddress: 'IP Address',
userAgent: 'User Agent',
createdAt: 'Created At',
detailTitle: 'Operation Log Detail',
getListFailed: 'Failed to get operation log list'
} }
}, },
modelUpload: { modelUpload: {

View File

@ -284,6 +284,7 @@ orderManagement: {
promptManagement: '提示词管理', promptManagement: '提示词管理',
productManagement: '产品管理', productManagement: '产品管理',
voucherManagement: '优惠券管理', voucherManagement: '优惠券管理',
operationLog: '操作日志',
logout: '退出登录', logout: '退出登录',
profile: '个人资料', profile: '个人资料',
settings: '设置', settings: '设置',
@ -922,6 +923,21 @@ orderManagement: {
hint: { hint: {
userIds: '多个用户ID用逗号分隔例如1,2,3' userIds: '多个用户ID用逗号分隔例如1,2,3'
} }
},
operationLog: {
title: '操作日志',
adminUserId: '管理员ID',
adminUsername: '管理员用户名',
operationType: '操作类型',
all: '全部',
resourceType: '资源类型',
resourceId: '资源ID',
description: '操作描述',
ipAddress: 'IP地址',
userAgent: '用户代理',
createdAt: '创建时间',
detailTitle: '操作日志详情',
getListFailed: '获取操作日志列表失败'
} }
}, },
modelUpload: { modelUpload: {

View File

@ -24,6 +24,7 @@ const AdminCommissionManagement = () => import('@/views/admin/AdminCommissionMan
const AdminPromptManagement = () => import('@/views/admin/AdminPromptManagement/AdminPromptManagement.vue') const AdminPromptManagement = () => import('@/views/admin/AdminPromptManagement/AdminPromptManagement.vue')
const AdminProductManagement = () => import('@/views/admin/ProductManagement/ProductManagement.vue') const AdminProductManagement = () => import('@/views/admin/ProductManagement/ProductManagement.vue')
const AdminVoucherManagement = () => import('@/views/admin/VoucherManagement/VoucherManagement.vue') const AdminVoucherManagement = () => import('@/views/admin/VoucherManagement/VoucherManagement.vue')
const AdminOperationLog = () => import('@/views/admin/AdminOperationLog/AdminOperationLog.vue')
//权限路由映射表 //权限路由映射表
export const permissionRoutes = [ export const permissionRoutes = [
{ {
@ -149,6 +150,17 @@ export const permissionRoutes = [
requiresAuth: true requiresAuth: true
} }
}, },
{
path: 'operation-log',
name: 'AdminOperationLog',
component: AdminOperationLog,
meta: {
title: 'admin.layout.operationLog',
icon: 'Document',
menuOrder: 7,
requiresAuth: true
}
},
{ {
path: 'permission', path: 'permission',
@ -156,7 +168,7 @@ export const permissionRoutes = [
meta: { meta: {
title: 'admin.layout.permission', title: 'admin.layout.permission',
icon: 'Lock', icon: 'Lock',
menuOrder: 7, menuOrder: 8,
isParent: true, isParent: true,
requiresAuth: true requiresAuth: true
}, },

View File

@ -0,0 +1,362 @@
<template>
<div class="operation-log">
<!-- 筛选和搜索 -->
<div class="log-filters">
<div class="filter-group">
<el-input
v-model="filters.adminUsername"
:placeholder="t('admin.operationLog.adminUsername')"
clearable
@keyup.enter="handleSearch"
@clear="handleSearch"
/>
</div>
<!-- <div class="filter-group">
<el-select
v-model="filters.operationType"
:placeholder="t('admin.operationLog.operationType')"
clearable
@change="handleSearch"
>
<el-option :label="t('admin.operationLog.all')" value="" />
<el-option label="CREATE" value="CREATE" />
<el-option label="UPDATE" value="UPDATE" />
<el-option label="DELETE" value="DELETE" />
<el-option label="QUERY" value="QUERY" />
</el-select>
</div> -->
<div class="filter-group">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="-"
:start-placeholder="t('admin.common.startDate')"
:end-placeholder="t('admin.common.endDate')"
value-format="YYYY-MM-DD HH:mm:ss"
popper-class="date-picker-popper"
:teleported="true"
@change="handleDateRangeChange"
/>
</div>
<div class="filter-actions">
<el-button :icon="Search" type="primary" @click="handleSearch">
{{ t('admin.common.search') }}
</el-button>
<el-button :icon="Refresh" @click="handleReset">
{{ t('admin.common.reset') }}
</el-button>
</div>
</div>
<!-- 操作日志列表 -->
<div class="log-table">
<el-table
:data="logList"
style="width: 100%"
v-loading="loading"
stripe
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="adminUsername" :label="t('admin.operationLog.adminUsername')" min-width="120" />
<el-table-column prop="operationTypeDesc" :label="t('admin.operationLog.operationType')" min-width="100">
<template #default="{ row }">
<el-tag :type="getOperationTypeTagType(row.operationType)">
{{ row.operationTypeDesc || row.operationType }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="resourceType" :label="t('admin.operationLog.resourceType')" min-width="120" />
<el-table-column prop="resourceId" :label="t('admin.operationLog.resourceId')" min-width="100" />
<el-table-column prop="description" :label="t('admin.operationLog.description')" min-width="200" show-overflow-tooltip />
<el-table-column prop="ipAddress" :label="t('admin.operationLog.ipAddress')" min-width="140" />
<el-table-column prop="createdAt" :label="t('admin.operationLog.createdAt')" min-width="180">
<template #default="{ row }">
{{ formatDateTime(row.createdAt) }}
</template>
</el-table-column>
<el-table-column :label="t('admin.common.actions')" min-width="100" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="handleViewDetail(row)">
{{ t('admin.common.detail') }}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 详情对话框 -->
<el-dialog
:title="t('admin.operationLog.detailTitle')"
v-model="detailDialogVisible"
width="700px"
top="5vh"
>
<div class="log-detail-container">
<el-descriptions :column="1" border>
<el-descriptions-item label="ID">
{{ selectedLog?.id }}
</el-descriptions-item>
<el-descriptions-item :label="t('admin.operationLog.adminUserId')">
{{ selectedLog?.adminUserId }}
</el-descriptions-item>
<el-descriptions-item :label="t('admin.operationLog.adminUsername')">
{{ selectedLog?.adminUsername }}
</el-descriptions-item>
<el-descriptions-item :label="t('admin.operationLog.operationType')">
<el-tag :type="getOperationTypeTagType(selectedLog?.operationType)">
{{ selectedLog?.operationTypeDesc || selectedLog?.operationType }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item :label="t('admin.operationLog.resourceType')">
{{ selectedLog?.resourceType }}
</el-descriptions-item>
<el-descriptions-item :label="t('admin.operationLog.resourceId')">
{{ selectedLog?.resourceId }}
</el-descriptions-item>
<el-descriptions-item :label="t('admin.operationLog.description')">
{{ selectedLog?.description }}
</el-descriptions-item>
<el-descriptions-item :label="t('admin.operationLog.ipAddress')">
{{ selectedLog?.ipAddress }}
</el-descriptions-item>
<el-descriptions-item :label="t('admin.operationLog.userAgent')">
<div class="user-agent">{{ selectedLog?.userAgent }}</div>
</el-descriptions-item>
<el-descriptions-item :label="t('admin.operationLog.createdAt')">
{{ formatDateTime(selectedLog?.createdAt) }}
</el-descriptions-item>
</el-descriptions>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="detailDialogVisible = false">{{ t('admin.common.close') }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
import { Search, Refresh } from '@element-plus/icons-vue'
import { AdminOperationLog } from './index.js'
const adminOperationLog = new AdminOperationLog()
const { t } = useI18n()
const loading = ref(false)
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
const dateRange = ref([])
const detailDialogVisible = ref(false)
const selectedLog = ref(null)
const filters = reactive({
adminUserId: '',
adminUsername: '',
operationType: '',
startTime: '',
endTime: '',
orderByColumn: 'createdAt',
isAsc: 'desc'
})
const logList = ref([])
const getOperationLogList = async () => {
loading.value = true
try {
const params = {
...filters,
pageSize: pageSize.value,
pageNum: currentPage.value
}
const res = await adminOperationLog.getOperationLogList(params)
if (res.code === 0 && res.data) {
logList.value = res.data.rows || []
total.value = res.data.total || 0
} else {
ElMessage.error(res.message || t('admin.operationLog.getListFailed'))
}
} catch (error) {
console.error('获取操作日志失败:', error)
ElMessage.error(t('admin.operationLog.getListFailed'))
} finally {
loading.value = false
}
}
const handleSearch = () => {
currentPage.value = 1
getOperationLogList()
}
const handleReset = () => {
filters.adminUserId = ''
filters.adminUsername = ''
filters.operationType = ''
filters.startTime = ''
filters.endTime = ''
dateRange.value = []
currentPage.value = 1
getOperationLogList()
}
const handleDateRangeChange = (value) => {
if (value && value.length === 2) {
filters.startTime = value[0]
filters.endTime = value[1]
} else {
filters.startTime = ''
filters.endTime = ''
}
}
const handleSizeChange = (val) => {
pageSize.value = val
currentPage.value = 1
getOperationLogList()
}
const handleCurrentChange = (val) => {
currentPage.value = val
getOperationLogList()
}
const handleViewDetail = (row) => {
selectedLog.value = row
detailDialogVisible.value = true
}
const getOperationTypeTagType = (type) => {
const typeMap = {
'CREATE': 'success',
'UPDATE': 'warning',
'DELETE': 'danger',
'QUERY': 'info'
}
return typeMap[type] || 'info'
}
const formatDateTime = (dateTime) => {
if (!dateTime) return '-'
const date = new Date(dateTime)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
onMounted(() => {
getOperationLogList()
})
</script>
<style scoped>
.operation-log {
padding: 20px;
background: #f5f7fa;
min-height: calc(100vh - 100px);
}
.log-filters {
display: flex;
gap: 12px;
margin-bottom: 20px;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
width: 100%;
}
.filter-group {
flex: 1;
min-width: 300px;
max-width: 400px;
}
.filter-actions {
display: flex;
gap: 8px;
}
.log-table {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 20px;
padding: 0 16px;
}
.log-detail-container {
padding: 10px 0;
}
.user-agent {
word-break: break-all;
max-width: 600px;
}
@media (max-width: 768px) {
.operation-log {
padding: 10px;
}
.log-filters {
flex-direction: column;
align-items: stretch;
}
.filter-group {
max-width: 100%;
min-width: 100%;
}
.filter-actions {
flex-direction: column;
}
.filter-actions .el-button {
width: 100%;
}
.log-table {
padding: 8px;
overflow-x: auto;
}
.pagination {
justify-content: center;
}
}
</style>
<style>
.date-picker-popper {
z-index: 9999 !important;
}
</style>

View File

@ -0,0 +1,48 @@
import { adminApi,requestUtils } from "@deotaland/utils";
export class AdminOperationLog {
constructor() {
}
// 分页查询操作日志列表
async getOperationLogList(data) {
let params = {
adminUserId: data.adminUserId ?? '',
adminUsername: data.adminUsername ?? '',
operationType: data.operationType ?? '',
startTime: data.startTime ?? '',
endTime: data.endTime ?? '',
pageSize: data.pageSize ?? 10,
pageNum: data.pageNum ?? 1,
orderByColumn: data.orderByColumn ?? '',
isAsc: data.isAsc ?? ''//排序的方向desc或者asc
}
return requestUtils.common(adminApi.default.getOperationLogList, params);
/*
返回示例:
{
"code": 0,
"success": true,
"data": {
"total": 9007199254740991,
"rows": [
{
"id": 1073741824,
"adminUserId": 1073741824,
"adminUsername": "string",
"operationType": "string",
"operationTypeDesc": "string",
"resourceType": "string",
"resourceId": "string",
"description": "string",
"ipAddress": "string",
"userAgent": "string",
"createdAt": "2026-01-09T09:10:39.219Z"
}
],
"code": 1073741824,
"msg": "string"
},
"message": "操作成功"
}
*/
}
}

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="https://draft-user.s3.us-east-2.amazonaws.com/images/2f8e057e-a677-44cd-b709-38245bdec423.svg" /> <link rel="icon" href="https://draft-user.s3.us-east-2.amazonaws.com/images/353c3066-f894-4937-8eb7-cd2eb634c4d7.webp" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, minimum-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, minimum-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

View File

@ -166,7 +166,7 @@ const fileInput = ref(null);
const needHook = ref(true); const needHook = ref(true);
const isGenerateDisabled = computed(() => { const isGenerateDisabled = computed(() => {
return !formData.value.prompt.trim(); return !formData.value.prompt.trim() && !formData.value.previewImage;
}); });
const handleIpTypeSelect = (type) => { const handleIpTypeSelect = (type) => {
@ -272,10 +272,9 @@ const handleOptimizePrompt = async () => {
}; };
const handleGenerate = () => { const handleGenerate = () => {
if (!formData.value.prompt.trim()) { if(!formData.value.prompt.trim() && !formData.value.previewImage){
return; return;
} }
emit('generate', { emit('generate', {
ipType: ipType.value, ipType: ipType.value,
prompt: formData.value.prompt, prompt: formData.value.prompt,

View File

@ -118,6 +118,7 @@ const formData = ref({
const isTouching = ref(false); const isTouching = ref(false);
const isControlsVisible = ref(false); const isControlsVisible = ref(false);
const cjimg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/14f98f33-06a7-4629-a42e-d7cfbced786f'; const cjimg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/14f98f33-06a7-4629-a42e-d7cfbced786f';
const e1cjimg='https://draft-user.s3.us-east-2.amazonaws.com/images/23130608-283e-41c3-bb28-8f2492f5e233.png'
const anTypeImg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/1e82b2b6-0e5d-4a62-b65f-098952eb2f67'; const anTypeImg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/1e82b2b6-0e5d-4a62-b65f-098952eb2f67';
// const humanTypeImg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/e3e60cc7-9777-41ba-9d1e-f5ffc92e4fac.webp'; // const humanTypeImg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/e3e60cc7-9777-41ba-9d1e-f5ffc92e4fac.webp';
// const humanTypeImg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/61770f50-4b87-40a0-9297-cabda0ec6317.webp' // const humanTypeImg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/61770f50-4b87-40a0-9297-cabda0ec6317.webp'
@ -234,6 +235,10 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false default: false
}, },
series:{//
type: String,
default: 'D1'
},
}); });
// //
@ -261,7 +266,10 @@ const handleGenerateImage = async () => {
if(iscjt){ if(iscjt){
props.cardData.imgyt&&referenceImages.push(props.cardData.imgyt); props.cardData.imgyt&&referenceImages.push(props.cardData.imgyt);
referenceImages.push(props.cardData.diyPromptImg); referenceImages.push(props.cardData.diyPromptImg);
referenceImages.push(cjimg); referenceImages.push({
D1:cjimg,
E1:e1cjimg,
}[props.series]);
} }
if(props.cardData.diyPromptText||props.cardData.addDiyPromptImg){ if(props.cardData.diyPromptText||props.cardData.addDiyPromptImg){
referenceImages.push(props.cardData.diyPromptImg); referenceImages.push(props.cardData.diyPromptImg);

View File

@ -23,10 +23,17 @@
<!-- 缩略图显示 --> <!-- 缩略图显示 -->
<div v-if="step.hasThumbnail" class="step-thumbnail"> <div v-if="step.hasThumbnail" class="step-thumbnail">
<img <img
v-if="series === 'D1'"
src="https://draft-user.s3.us-east-2.amazonaws.com/images/4abf3da6-6abe-4604-adb9-554cf49d9743.webp" src="https://draft-user.s3.us-east-2.amazonaws.com/images/4abf3da6-6abe-4604-adb9-554cf49d9743.webp"
alt="产品零件示例图" alt="产品零件示例图"
class="thumbnail-image" class="thumbnail-image"
/> />
<img
v-else-if="series === 'E1'"
src="https://draft-user.s3.us-east-2.amazonaws.com/images/1a359f21-1dcd-4167-90ca-7e5eb86a45db.png"
alt="产品零件示例图"
class="thumbnail-image"
/>
<p class="thumbnail-caption">{{ t('orderProcess.steps.inspection.thumbnailCaption') }}</p> <p class="thumbnail-caption">{{ t('orderProcess.steps.inspection.thumbnailCaption') }}</p>
</div> </div>
</div> </div>
@ -59,10 +66,12 @@ import {
Picture, Picture,
Van Van
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import service from '@deotaland/utils/src/utils/request'
const { t } = useI18n() const { t } = useI18n()
const props = defineProps({ const props = defineProps({
show: { type: Boolean, default: false }, show: { type: Boolean, default: false },
modelData: { type: Object, default: null } modelData: { type: Object, default: null },
series: { type: String, default: '' }
}) })
const emit = defineEmits(['close', 'acknowledge']) const emit = defineEmits(['close', 'acknowledge'])
const handleOverlayClick = () => onClose() const handleOverlayClick = () => onClose()
@ -91,12 +100,15 @@ const processSteps = computed(() => [
description: t('orderProcess.steps.scheduling.description'), description: t('orderProcess.steps.scheduling.description'),
time: t('orderProcess.steps.scheduling.time') time: t('orderProcess.steps.scheduling.time')
}, },
// { {
// icon: Setting, icon: Setting,
// title: t('orderProcess.steps.production.title'), title: t('orderProcess.steps.production.title'),
// description: t('orderProcess.steps.production.description'), description: t('orderProcess.steps.production.description'),
// time: t('orderProcess.steps.production.time') time: {
// }, D1: t('orderProcess.steps.production.time'),
E1: t('orderProcess.steps.production.time2'),
}[props.series]
},
{ {
icon: Picture, icon: Picture,
title: t('orderProcess.steps.inspection.title'), title: t('orderProcess.steps.inspection.title'),

View File

@ -14,7 +14,7 @@
<router-link to="/" class="brand-link"> <router-link to="/" class="brand-link">
<div class="brand-logo"> <div class="brand-logo">
<div class="logo-icon"> <div class="logo-icon">
<img src="@/assets/logo.webp" alt="Logo" class="logo-image" /> <img src="https://draft-user.s3.us-east-2.amazonaws.com/images/353c3066-f894-4937-8eb7-cd2eb634c4d7.webp" alt="Logo" class="logo-image" />
</div> </div>
<span class="brand-name">{{ t('app.title') }}</span> <span class="brand-name">{{ t('app.title') }}</span>
</div> </div>

View File

@ -204,7 +204,8 @@ export default {
production: { production: {
title: '模型制作', title: '模型制作',
description: '专业团队使用高精度3D打印机制作您的定制模型确保每个细节都完美呈现。', description: '专业团队使用高精度3D打印机制作您的定制模型确保每个细节都完美呈现。',
time: '7-10个工作日' time: '7-10个工作日',
time2: '1个工作日',
}, },
inspection: { inspection: {
title: '产品检测包装', title: '产品检测包装',
@ -1755,7 +1756,8 @@ export default {
production: { production: {
title: 'Model Production', title: 'Model Production',
description: 'Professional team uses high-precision 3D printers to create your custom model, ensuring every detail is perfectly presented.', description: 'Professional team uses high-precision 3D printers to create your custom model, ensuring every detail is perfectly presented.',
time: '7-10 working days' time: '7-10 working days',
time2: '1-2 working days',
}, },
inspection: { inspection: {
title: 'Product Inspection & Packaging', title: 'Product Inspection & Packaging',

View File

@ -23,11 +23,14 @@ import 'nprogress/nprogress.css'
import {ElMessage,ElLoading } from 'element-plus' import {ElMessage,ElLoading } from 'element-plus'
import dtUI from '@deotaland/ui' import dtUI from '@deotaland/ui'
import '@deotaland/ui/style.css' import '@deotaland/ui/style.css'
import { environmentUtils } from '@deotaland/utils'; import { environmentUtils,WechatBus,isWeChatBrowser } from '@deotaland/utils';
const app = createApp(App) const app = createApp(App)
window.setElMessage = (options={})=>{ window.setElMessage = (options={})=>{
ElMessage[options.type || 'info'](options.message || '请求失败') ElMessage[options.type || 'info'](options.message || '请求失败')
} }
if(isWeChatBrowser()){
new WechatBus()
}
// environmentUtils.detectEnvironment().then((env)=>{ // environmentUtils.detectEnvironment().then((env)=>{
// console.log('当前环境:', env) // console.log('当前环境:', env)
// }) // })

View File

@ -48,7 +48,7 @@
<!-- 忘记密码和注册链接 --> <!-- 忘记密码和注册链接 -->
<div class="auth-links"> <div class="auth-links">
<div class="auth-links-row"> <div class="auth-links-row">
<button <!-- <button
v-if="showPhoneLogin" v-if="showPhoneLogin"
type="button" type="button"
class="auth-link phone-login-link" class="auth-link phone-login-link"
@ -56,7 +56,7 @@
> >
<el-icon class="link-icon"><Phone /></el-icon> <el-icon class="link-icon"><Phone /></el-icon>
<span>{{ t('login.phone_login_link') }}</span> <span>{{ t('login.phone_login_link') }}</span>
</button> </button> -->
<button <button
type="button" type="button"
class="auth-link forgot-password-link" class="auth-link forgot-password-link"

View File

@ -5,7 +5,7 @@
<div class="welcome-content"> <div class="welcome-content">
<div class="welcome-left"> <div class="welcome-left">
<div class="welcome-logo"> <div class="welcome-logo">
<img src="@/assets/logo.webp" alt="Logo" class="welcome-logo-image" /> <img src="https://draft-user.s3.us-east-2.amazonaws.com/images/353c3066-f894-4937-8eb7-cd2eb634c4d7.webp" alt="Logo" class="welcome-logo-image" />
<div class="logo-glow"></div> <div class="logo-glow"></div>
</div> </div>
<h1 class="welcome-title"> <h1 class="welcome-title">

View File

@ -72,6 +72,7 @@
@delete="handleDeleteCard(index)" @delete="handleDeleteCard(index)"
@delete-card="handleDeleteCard(index)" @delete-card="handleDeleteCard(index)"
:combinedPromptJson="combinedPromptJson" :combinedPromptJson="combinedPromptJson"
:series="series"
@handlePartialEdit="(imageUrl) => handlePartialEdit(imageUrl, index)" @handlePartialEdit="(imageUrl) => handlePartialEdit(imageUrl, index)"
@customize-to-home="handleCustomizeToHome(index)" @customize-to-home="handleCustomizeToHome(index)"
@preview-image="handlePreviewImage" @preview-image="handlePreviewImage"
@ -136,6 +137,7 @@
/> />
<!-- 定制到家弹窗 --> <!-- 定制到家弹窗 -->
<OrderProcessModal <OrderProcessModal
:series="series"
:show="showOrderProcessModal" :show="showOrderProcessModal"
:modelData="CustomizeModalData" :modelData="CustomizeModalData"
@close="showOrderProcessModal=false" @close="showOrderProcessModal=false"

View File

@ -91,6 +91,7 @@
:key="card.id" :key="card.id"
:image-url="card.imageUrl" :image-url="card.imageUrl"
:card-data="card" :card-data="card"
:series="series"
:combinedPromptJson="combinedPromptJson" :combinedPromptJson="combinedPromptJson"
@save-project="(item)=>{handleSaveProject(index,item,'image')}" @save-project="(item)=>{handleSaveProject(index,item,'image')}"
@customize-to-home="handleCustomizeToHome(index)" @customize-to-home="handleCustomizeToHome(index)"
@ -124,6 +125,7 @@
@close="showPurchaseModal=false" /> @close="showPurchaseModal=false" />
<!-- 定制到家弹窗 --> <!-- 定制到家弹窗 -->
<OrderProcessModal <OrderProcessModal
:series="series"
:show="showOrderProcessModal" :show="showOrderProcessModal"
:modelData="CustomizeModalData" :modelData="CustomizeModalData"
@close="showOrderProcessModal=false" @close="showOrderProcessModal=false"
@ -182,7 +184,7 @@ import ThemeToggle from '@/components/ui/ThemeToggle.vue';
import AddModal from '@/components/AddModal/index.vue'; import AddModal from '@/components/AddModal/index.vue';
import PurchaseModal from '@/components/PurchaseModal/index.vue'; import PurchaseModal from '@/components/PurchaseModal/index.vue';
import OrderProcessModal from '@/components/OrderProcessModal/index.vue'; import OrderProcessModal from '@/components/OrderProcessModal/index.vue';
import {WechatBus,isWeChatBrowser} from '@deotaland/utils';
// //
import ShuCard from '@/components/IPCard/shu.vue'; import ShuCard from '@/components/IPCard/shu.vue';
const { t } = useI18n(); const { t } = useI18n();
@ -259,6 +261,13 @@ const getGenerateCount = async ()=>{
} }
// //
const downloadImage = async (url) => { const downloadImage = async (url) => {
if(isWeChatBrowser()){
WechatBus.BusWechartForNavigate('/pages/bus/index',{
method: 'download',
url: url,
})
return;
}
try { try {
const blobUrl = await fileServer.fetchImage(url); const blobUrl = await fileServer.fetchImage(url);
const link = document.createElement('a'); const link = document.createElement('a');

View File

@ -385,7 +385,8 @@ import { ElMessage } from 'element-plus'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { UserController } from './index.js' import { UserController } from './index.js'
import {ModernHome} from '../ModernHome/index.js' import {ModernHome} from '../ModernHome/index.js'
import { FileServer } from '@deotaland/utils' import { FileServer,WechatBus } from '@deotaland/utils'
const wechatBus = new WechatBus();
const router = useRouter() const router = useRouter()
const modernHome = new ModernHome() const modernHome = new ModernHome()
const { t } = useI18n() const { t } = useI18n()

View File

@ -66,8 +66,8 @@ export default defineConfig({
// 配置代理解决CORS问题 // 配置代理解决CORS问题
proxy: { proxy: {
'/api': { '/api': {
// target: 'https://api.deotaland.ai', target: 'https://api.deotaland.ai',
target: 'http://api.deotaland.local', // target: 'http://api.deotaland.local',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '') rewrite: (path) => path.replace(/^\/api/, '')
} }

View File

@ -8,5 +8,6 @@ const order = {
getInviteCodeList:{url:'/api-base/admin/invite-code/list',method:'GET'},//分页查询邀请码列表 getInviteCodeList:{url:'/api-base/admin/invite-code/list',method:'GET'},//分页查询邀请码列表
generateInviteCode:{url:'/api-base/admin/invite-code/generate',method:'POST',isLoading:true},//为用户生成邀请码(支持批量生成) generateInviteCode:{url:'/api-base/admin/invite-code/generate',method:'POST',isLoading:true},//为用户生成邀请码(支持批量生成)
deleteInviteCode:{url:'/api-base/admin/invite-code/CODEID',method:'DELETE',isLoading:true},//根据邀请码ID删除邀请码 deleteInviteCode:{url:'/api-base/admin/invite-code/CODEID',method:'DELETE',isLoading:true},//根据邀请码ID删除邀请码
getOperationLogList:{url:'/api-base/admin/operation-log/list',method:'GET'},//分页查询操作日志列表
} }
export default order; export default order;

View File

@ -23,6 +23,7 @@ import prompt from './servers/prompt.js';
import { PayServer } from './servers/payserver.js'; import { PayServer } from './servers/payserver.js';
import { LogistIcsService } from './servers/logisticsservice.js'; import { LogistIcsService } from './servers/logisticsservice.js';
import * as orderStatus from './utils/orderStatus.js'; import * as orderStatus from './utils/orderStatus.js';
import { WechatBus } from './utils/wechaBus.js';
// 合并所有工具函数 // 合并所有工具函数
const deotalandUtils = { const deotalandUtils = {
string: stringUtils, string: stringUtils,
@ -44,6 +45,8 @@ const deotalandUtils = {
PayServer, PayServer,
LogistIcsService, LogistIcsService,
orderStatus, orderStatus,
WechatBus,
isWeChatBrowser,
// 全局常用方法 // 全局常用方法
debounce: stringUtils.debounce || createDebounce(), debounce: stringUtils.debounce || createDebounce(),
throttle: stringUtils.throttle || createThrottle(), throttle: stringUtils.throttle || createThrottle(),
@ -77,8 +80,13 @@ export {
PayServer, PayServer,
LogistIcsService, LogistIcsService,
orderStatus, orderStatus,
WechatBus,
isWeChatBrowser,
}
function isWeChatBrowser() {
const ua = navigator.userAgent.toLowerCase()
return ua.indexOf('micromessenger') !== -1
} }
/** /**
* 创建防抖函数 * 创建防抖函数
*/ */

View File

@ -1,45 +1,25 @@
export class WechatBus { export class WechatBus {
static wx = window.wx || window.jWeixin
constructor() { constructor() {
this.wx = null if (!WechatBus.wx) {
this.isReady = false this.loadJSSDK()
this.init()
}
init() {
if (typeof window !== 'undefined') {
this.wx = window.wx || window.jWeixin
if (this.wx && this.wx.miniProgram) {
this.isReady = true
console.log('WechatBus initialized successfully')
} else {
console.warn('Wechat JSSDK not loaded or miniProgram not available')
}
} }
} }
// 加载微信JSSDK
checkReady() {
if (!this.isReady || !this.wx || !this.wx.miniProgram) {
throw new Error('Wechat JSSDK is not ready. Please ensure JSSDK is loaded.')
}
}
loadJSSDK(src = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js') { loadJSSDK(src = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js') {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
reject(new Error('Window object not available')) reject(new Error('Window object not available'))
return return
} }
if (window.wx || window.jWeixin) { if (window.wx || window.jWeixin) {
this.init()
resolve(true) resolve(true)
return return
} }
const script = document.createElement('script') const script = document.createElement('script')
script.src = src script.src = src
script.onload = () => { script.onload = () => {
this.init() WechatBus.wx = window.wx || window.jWeixin
resolve(true) resolve(true)
} }
script.onerror = () => { script.onerror = () => {
@ -48,47 +28,20 @@ export class WechatBus {
document.head.appendChild(script) document.head.appendChild(script)
}) })
} }
static objectToUrlParams(obj, prefix = '') {
config(config) {
this.checkReady()
return new Promise((resolve, reject) => {
this.wx.config({
debug: config.debug || false,
appId: config.appId,
timestamp: config.timestamp,
nonceStr: config.nonceStr,
signature: config.signature,
jsApiList: config.jsApiList || []
})
this.wx.ready(() => {
console.log('Wechat JSSDK config ready')
resolve(true)
})
this.wx.error((res) => {
console.error('Wechat JSSDK config error:', res)
reject(res)
})
})
}
objectToUrlParams(obj, prefix = '') {
const params = [] const params = []
for (const key in obj) { for (const key in obj) {
if (obj.hasOwnProperty(key)) { if (obj.hasOwnProperty(key)) {
const value = obj[key] const value = obj[key]
const paramKey = prefix ? `${prefix}[${key}]` : key const paramKey = prefix ? `${prefix}[${key}]` : key
if (value !== null && value !== undefined) { if (value !== null && value !== undefined) {
if (typeof value === 'object' && !Array.isArray(value)) { if (typeof value === 'object' && !Array.isArray(value)) {
const nestedParams = this.objectToUrlParams(value, paramKey) const nestedParams = WechatBus.objectToUrlParams(value, paramKey)
params.push(...nestedParams) params.push(...nestedParams)
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {
value.forEach((item, index) => { value.forEach((item, index) => {
if (typeof item === 'object') { if (typeof item === 'object') {
const nestedParams = this.objectToUrlParams(item, `${paramKey}[${index}]`) const nestedParams = WechatBus.objectToUrlParams(item, `${paramKey}[${index}]`)
params.push(...nestedParams) params.push(...nestedParams)
} else { } else {
params.push(`${encodeURIComponent(paramKey)}[${index}]=${encodeURIComponent(item)}`) params.push(`${encodeURIComponent(paramKey)}[${index}]=${encodeURIComponent(item)}`)
@ -103,303 +56,18 @@ export class WechatBus {
return params return params
} }
// 导航到微信小程序页面并且携带通信参数
navigateTo(url, data = {}) { static BusWechartForNavigate(url, data = {}) {
this.checkReady()
let finalUrl = url let finalUrl = url
if (data && typeof data === 'object' && Object.keys(data).length > 0) { if (data && typeof data === 'object' && Object.keys(data).length > 0) {
const params = this.objectToUrlParams(data) const params = WechatBus.objectToUrlParams(data)
if (params.length > 0) { if (params.length > 0) {
const separator = url.includes('?') ? '&' : '?' const separator = url.includes('?') ? '&' : '?'
finalUrl = url + separator + params.join('&') finalUrl = url + separator + params.join('&')
} }
} }
WechatBus.wx.miniProgram.navigateTo({
return new Promise((resolve, reject) => { url: finalUrl
this.wx.miniProgram.navigateTo({
url: finalUrl,
success: (res) => resolve(res),
fail: (error) => reject(error)
})
}) })
} }
}
navigateBack(delta = 1) {
this.checkReady()
return new Promise((resolve, reject) => {
this.wx.miniProgram.navigateBack({
delta,
success: (res) => resolve(res),
fail: (error) => reject(error)
})
})
}
redirectTo(url, data = {}) {
this.checkReady()
let finalUrl = url
if (data && typeof data === 'object' && Object.keys(data).length > 0) {
const params = this.objectToUrlParams(data)
if (params.length > 0) {
const separator = url.includes('?') ? '&' : '?'
finalUrl = url + separator + params.join('&')
}
}
return new Promise((resolve, reject) => {
this.wx.miniProgram.redirectTo({
url: finalUrl,
success: (res) => resolve(res),
fail: (error) => reject(error)
})
})
}
switchTab(url) {
this.checkReady()
return new Promise((resolve, reject) => {
this.wx.miniProgram.switchTab({
url,
success: (res) => resolve(res),
fail: (error) => reject(error)
})
})
}
reLaunch(url, data = {}) {
this.checkReady()
let finalUrl = url
if (data && typeof data === 'object' && Object.keys(data).length > 0) {
const params = this.objectToUrlParams(data)
if (params.length > 0) {
const separator = url.includes('?') ? '&' : '?'
finalUrl = url + separator + params.join('&')
}
}
return new Promise((resolve, reject) => {
this.wx.miniProgram.reLaunch({
url: finalUrl,
success: (res) => resolve(res),
fail: (error) => reject(error)
})
})
}
postMessage(data) {
this.checkReady()
return new Promise((resolve, reject) => {
try {
this.wx.miniProgram.postMessage({
data
})
resolve(true)
} catch (error) {
console.error('Post message error:', error)
reject(error)
}
})
}
getEnv() {
this.checkReady()
return this.wx.miniProgram.getEnv()
}
getAccountInfoSync() {
this.checkReady()
return this.wx.getAccountInfoSync()
}
setStorageSync(key, data) {
this.checkReady()
return new Promise((resolve, reject) => {
try {
this.wx.miniProgram.setStorageSync({
key,
data
})
resolve(true)
} catch (error) {
console.error('Set storage error:', error)
reject(error)
}
})
}
getStorageSync(key) {
this.checkReady()
return new Promise((resolve, reject) => {
try {
const data = this.wx.miniProgram.getStorageSync({
key
})
resolve(data)
} catch (error) {
console.error('Get storage error:', error)
reject(error)
}
})
}
removeStorageSync(key) {
this.checkReady()
return new Promise((resolve, reject) => {
try {
this.wx.miniProgram.removeStorageSync({
key
})
resolve(true)
} catch (error) {
console.error('Remove storage error:', error)
reject(error)
}
})
}
clearStorageSync() {
this.checkReady()
return new Promise((resolve, reject) => {
try {
this.wx.miniProgram.clearStorageSync()
resolve(true)
} catch (error) {
console.error('Clear storage error:', error)
reject(error)
}
})
}
chooseImage(options = {}) {
this.checkReady()
return new Promise((resolve, reject) => {
const defaultOptions = {
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
...options
}
this.wx.chooseImage({
...defaultOptions,
success: (res) => resolve(res),
fail: (error) => reject(error)
})
})
}
previewImage(options) {
this.checkReady()
return new Promise((resolve, reject) => {
this.wx.previewImage({
...options,
success: (res) => resolve(res),
fail: (error) => reject(error)
})
})
}
getLocation(options = {}) {
this.checkReady()
return new Promise((resolve, reject) => {
const defaultOptions = {
type: 'wgs84',
...options
}
this.wx.getLocation({
...defaultOptions,
success: (res) => resolve(res),
fail: (error) => reject(error)
})
})
}
openLocation(options) {
this.checkReady()
return new Promise((resolve, reject) => {
this.wx.openLocation({
...options,
success: (res) => resolve(res),
fail: (error) => reject(error)
})
})
}
scanQRCode(options = {}) {
this.checkReady()
return new Promise((resolve, reject) => {
const defaultOptions = {
needResult: 1,
scanType: ['qrCode', 'barCode'],
...options
}
this.wx.scanQRCode({
...defaultOptions,
success: (res) => resolve(res),
fail: (error) => reject(error)
})
})
}
chooseWXPay(options) {
this.checkReady()
return new Promise((resolve, reject) => {
this.wx.chooseWXPay({
...options,
success: (res) => resolve(res),
fail: (error) => reject(error)
})
})
}
updateAppMessageShareData(options) {
this.checkReady()
return new Promise((resolve, reject) => {
this.wx.updateAppMessageShareData({
...options,
success: (res) => resolve(res),
fail: (error) => reject(error)
})
})
}
updateTimelineShareData(options) {
this.checkReady()
return new Promise((resolve, reject) => {
this.wx.updateTimelineShareData({
...options,
success: (res) => resolve(res),
fail: (error) => reject(error)
})
})
}
closeWindow() {
this.checkReady()
this.wx.closeWindow()
}
hideMenuItems(menuList = []) {
this.checkReady()
this.wx.hideMenuItems({
menuList
})
}
showMenuItems(menuList = []) {
this.checkReady()
this.wx.showMenuItems({
menuList
})
}
}
const wechatBus = new WechatBus()
export default wechatBus
export { WechatBus }