tangning 1 ماه پیش
والد
کامیت
1de68fa21d
57فایلهای تغییر یافته به همراه2181 افزوده شده و 1470 حذف شده
  1. 1 0
      package.json
  2. 166 0
      scripts/build-en-locale.cjs
  3. 306 0
      scripts/en-translations-manual.cjs
  4. 51 0
      scripts/patch-en-chinese.cjs
  5. 8 6
      src/api/mall/index.js
  6. 7 0
      src/api/scene/index.js
  7. 6 0
      src/assets/style/index.less
  8. 36 37
      src/components/AddressModal.vue
  9. 12 6
      src/components/EmptyData/index.vue
  10. 15 9
      src/components/myModal.vue
  11. 6 6
      src/layouts/footer.vue
  12. 8 8
      src/layouts/header.vue
  13. 70 25
      src/layouts/menu.vue
  14. 1 0
      src/stores/user.js
  15. 2 2
      src/views/404.vue
  16. 11 6
      src/views/account/components/header.vue
  17. 6 4
      src/views/account/index.vue
  18. 17 17
      src/views/account/login.vue
  19. 4 2
      src/views/account/qrcode.vue
  20. 14 14
      src/views/account/register.vue
  21. 28 55
      src/views/device/components/bindCameraModal.vue
  22. 1 1
      src/views/device/components/cooperationModal.vue
  23. 5 13
      src/views/device/index.vue
  24. 6 2
      src/views/equity/components/bindCameraModal.vue
  25. 6 6
      src/views/equity/index.vue
  26. 8 5
      src/views/home/components/notice.vue
  27. 39 29
      src/views/home/index.vue
  28. 13 10
      src/views/information/components/brandInfo.vue
  29. 37 42
      src/views/information/components/info.vue
  30. 6 6
      src/views/information/index.vue
  31. 24 201
      src/views/mall/camera.vue
  32. 21 13
      src/views/mall/cart.vue
  33. 5 2
      src/views/mall/components/Parts.vue
  34. 2 2
      src/views/mall/components/cart/confirmOrder.vue
  35. 10 13
      src/views/mall/components/cart/list.vue
  36. 11 6
      src/views/mall/components/header.vue
  37. 10 8
      src/views/mall/components/shopInfo.vue
  38. 40 55
      src/views/mall/components/shopMemberCard.vue
  39. 19 3
      src/views/mall/components/valueAdded.vue
  40. 142 436
      src/views/mall/data.ts
  41. 5 1
      src/views/mall/details.vue
  42. 14 33
      src/views/mall/goods.vue
  43. 7 7
      src/views/mall/home.vue
  44. 3 3
      src/views/mall/membership.vue
  45. 3 3
      src/views/mall/membershipConfig.ts
  46. 20 4
      src/views/mall/sercice.vue
  47. 17 41
      src/views/order/components/InvoicePreview.vue
  48. 48 76
      src/views/order/components/invoiceModal.vue
  49. 25 37
      src/views/order/details.vue
  50. 3 3
      src/views/order/list.vue
  51. 22 48
      src/views/scene/components/cooperation.vue
  52. 32 66
      src/views/scene/components/moveSceneModal.vue
  53. 7 10
      src/views/scene/components/renameModal.vue
  54. 23 17
      src/views/scene/components/uploadModal.vue
  55. 693 26
      src/views/scene/mesh.vue
  56. 56 22
      src/views/scene/more.vue
  57. 23 23
      src/views/scene/obj.vue

+ 1 - 0
package.json

@@ -14,6 +14,7 @@
     "build:cn-prod": "vite build --mode cn-prod",
     "build:cn-prod": "vite build --mode cn-prod",
     "build:intl-test": "vite build --mode intl-test",
     "build:intl-test": "vite build --mode intl-test",
     "build:intl-prod": "vite build --mode intl-prod",
     "build:intl-prod": "vite build --mode intl-prod",
+    "i18n:en": "node scripts/build-en-locale.cjs && node scripts/patch-en-chinese.cjs",
     "preview": "vite preview"
     "preview": "vite preview"
   },
   },
   "dependencies": {
   "dependencies": {

+ 166 - 0
scripts/build-en-locale.cjs

@@ -0,0 +1,166 @@
+/**
+ * 根据 zh.json + 旧项目 common/lang 生成 en.json
+ * 用法: node scripts/build-en-locale.cjs
+ */
+const fs = require('fs');
+const root = 'd:/project/2026项目/DeepAICloud';
+const zhPath = `${root}/src/locales/zh.json`;
+const enPath = `${root}/src/locales/en.json`;
+const oldEnPath = 'd:/project/2021项目/四维看看官网/webSite/common/lang/en.json';
+const oldZhPath = 'd:/project/2021项目/四维看看官网/webSite/common/lang/zh.json';
+
+const zh = JSON.parse(fs.readFileSync(zhPath, 'utf8'));
+const oldEn = JSON.parse(fs.readFileSync(oldEnPath, 'utf8'));
+const oldZh = JSON.parse(fs.readFileSync(oldZhPath, 'utf8'));
+
+const oldPcEn = oldEn.pc || {};
+const oldPcZh = oldZh.pc || {};
+
+function buildTextMap(zhObj, enObj, map = new Map()) {
+  if (typeof zhObj === 'string' && typeof enObj === 'string' && zhObj.trim()) {
+    map.set(zhObj, enObj);
+    return map;
+  }
+  if (zhObj && enObj && typeof zhObj === 'object' && typeof enObj === 'object' && !Array.isArray(zhObj)) {
+    for (const key of Object.keys(zhObj)) {
+      if (enObj[key] !== undefined) buildTextMap(zhObj[key], enObj[key], map);
+    }
+  }
+  return map;
+}
+
+const zhToEn = buildTextMap(oldPcZh, oldPcEn);
+
+const zhToEnManual = require('./en-translations-manual.cjs');
+
+const manual = {
+  ...zhToEnManual,
+  '欢迎使用深智云': 'Welcome to DeepAI Cloud',
+  '语言': 'Language',
+  '首页': 'Home',
+  '设置': 'Settings',
+  '操作成功': 'Operation successful',
+  '失败': 'Failed',
+  '加载中...': 'Loading...',
+  '付款成功后5个工作日内发货,默认顺丰快递包邮':
+    'Ships within 5 business days after payment. SF Express shipping included.',
+  '立即购买': 'Buy Now',
+  '联系客服': 'Contact Support',
+  '限时免费': 'Limited-time free',
+  '咨询客服': 'Contact support',
+  '¥{price}': '¥{price}',
+  '您的购物车空空如也': 'Your cart is empty',
+  '赶紧行动吧': 'Start shopping now',
+  '四维时代商城': '4Dage Mall',
+  '充分整合硬件、软件、增值服务,打造四维空间智能生态':
+    'Integrating hardware, software, and value-added services to build an intelligent 4D spatial ecosystem',
+  '销售会员': 'Membership',
+  '耗材配件': 'Consumables & Accessories',
+  '增值服务': 'Value-added Services',
+  '相机配件': 'Camera Accessories',
+  '颜色': 'Color',
+  '配件': 'Accessories',
+  '简介': 'Introduction',
+  '联系我们': 'Contact Us',
+  '我的账户': 'My Account',
+  '退出登录': 'Log Out',
+  '常见问题': 'FAQ',
+  '元/年': 'CNY/year',
+  '云容量': 'Cloud Storage',
+  '品牌信息': 'Brand Info',
+  '优先计算': 'Priority Computing',
+  '离线包下载': 'Offline Package Download',
+  '立即体验': 'Try Now',
+  '激光+全景': 'LiDAR + Panorama',
+  '全景': 'Panorama',
+  '激光': 'LiDAR',
+  '免费10次': '10 free downloads',
+  '{price}元/次': '{price} CNY/time',
+  '我已支付': 'I have paid',
+  '请在新窗口完成支付,完成后可点击下方按钮确认':
+    'Please complete payment in the new window, then click the button below to confirm.',
+  '重新打开支付页': 'Reopen payment page',
+  '同意《四维看看销售协议》': 'I agree to the 4DKanKan Sales Agreement',
+};
+
+function translateString(s) {
+  if (typeof s !== 'string') return s;
+  if (manual[s]) return manual[s];
+  if (zhToEn.has(s)) return zhToEn.get(s);
+  return s;
+}
+
+function deepTranslate(obj) {
+  if (typeof obj === 'string') return translateString(obj);
+  if (Array.isArray(obj)) return obj.map(deepTranslate);
+  if (obj && typeof obj === 'object') {
+    const out = {};
+    for (const [k, v] of Object.entries(obj)) out[k] = deepTranslate(v);
+    return out;
+  }
+  return obj;
+}
+
+function overlayByKey(zhPart, enPart, refPart) {
+  if (!zhPart || !enPart || !refPart) return;
+  for (const key of Object.keys(zhPart)) {
+    if (refPart[key] === undefined) continue;
+    if (typeof zhPart[key] === 'string' && typeof refPart[key] === 'string') {
+      enPart[key] = refPart[key];
+    } else if (typeof zhPart[key] === 'object' && zhPart[key] !== null && !Array.isArray(zhPart[key])) {
+      if (!enPart[key] || typeof enPart[key] !== 'object') enPart[key] = {};
+      overlayByKey(zhPart[key], enPart[key], refPart[key]);
+    }
+  }
+}
+
+const en = deepTranslate(JSON.parse(JSON.stringify(zh)));
+
+for (const key of Object.keys(zh)) {
+  if (typeof zh[key] === 'object' && oldPcEn[key]) {
+    overlayByKey(zh[key], en[key], oldPcEn[key]);
+  }
+}
+
+if (oldPcEn.manage && zh.scene) {
+  overlayByKey(zh.scene, en.scene, {
+    sceneAdmin: oldPcEn.manage.sceneAdmin,
+    myScenes: oldPcEn.manage.sceneAdmin,
+    sceneDownload: oldPcEn.manage.sceneDownload,
+  });
+}
+
+if (oldPcEn.manage && zh.deviceAdmin) {
+  overlayByKey(zh.deviceAdmin, en.deviceAdmin, oldPcEn.manage.deviceAdmin || {});
+}
+
+if (oldPcEn.manage && zh.member) {
+  overlayByKey(zh.member, en.member, oldPcEn.manage.member || oldPcEn.member || {});
+}
+
+if (en.login && en.login.agreeXieyi) {
+  en.toast = en.toast || {};
+  en.toast.agreeXieyi = en.login.agreeXieyi;
+}
+
+fs.writeFileSync(enPath, `${JSON.stringify(en, null, 2)}\n`);
+
+const untranslated = [];
+function countUntranslated(zhPart, enPart, path = '') {
+  if (typeof zhPart === 'string' && typeof enPart === 'string') {
+    if (zhPart === enPart && /[\u4e00-\u9fff]/.test(zhPart)) {
+      untranslated.push(path);
+    }
+    return;
+  }
+  if (zhPart && enPart && typeof zhPart === 'object' && !Array.isArray(zhPart)) {
+    for (const key of Object.keys(zhPart)) {
+      countUntranslated(zhPart[key], enPart[key], path ? `${path}.${key}` : key);
+    }
+  }
+}
+countUntranslated(zh, en);
+console.log(`en.json generated. Untranslated strings: ${untranslated.length}`);
+if (untranslated.length > 0 && untranslated.length <= 20) {
+  console.log(untranslated.join('\n'));
+}

+ 306 - 0
scripts/en-translations-manual.cjs

@@ -0,0 +1,306 @@
+module.exports = {
+  '我已阅读并同意': 'I have read and agree to',
+  '《四维看看用户协议》': '4DKanKan Pro User Agreement',
+  '账号密码': 'Account & Password',
+  '登录成功': 'Login successful',
+  '登录失败,请重试!': 'Login failed. Please try again.',
+  '验证码只能输入数字': 'Verification code must be numeric',
+  '请输入确认密码': 'Please confirm your password',
+  '两次输入密码不一致': 'Passwords do not match',
+  '阅读至最底部,“确定”方可生效': 'Scroll to the bottom and click Confirm to proceed',
+  '我已阅读并同意四维看看用户协议': 'I have read and agree to the 4DKanKan Pro User Agreement',
+  '还没有账号?': "Don't have an account?",
+  '立即注册': 'Register now',
+  '直接登录 »': 'Log in now »',
+  '密码(8-16位数字和大小写字母)': 'Password (8-16 characters with letters and digits)',
+  '再次输入密码(8-16位数字和大小写字母)': 'Re-enter password (8-16 characters with letters and digits)',
+  '点云': 'Point Cloud',
+  'Mesh场景': 'Mesh Scene',
+  '更新于{time}': 'Updated at {time}',
+  '离线包': 'Offline Package',
+  '上传文件': 'Upload Files',
+  '将文件拖到此处,或': 'Drag files here, or',
+  '点击上传': 'click to upload',
+  '支持 E57、LAS、PLY 格式的点云上传支持四维看看系列相机导出的原始数据上传, 文件格式为 ZIP':
+    'Supports point cloud uploads in E57, LAS, and PLY formats, and raw data exported from 4DKanKan cameras in ZIP format',
+  '正在上传, 请勿关闭此页面': 'Uploading. Please do not close this page',
+  '续传': 'Resume',
+  '请先上传文件': 'Please upload a file first',
+  '已提交上传任务': 'Upload task submitted',
+  '添加协作': 'Add Collaborator',
+  '协作时间': 'Collaboration Time',
+  '已取消与 {userName} 的协作': 'Collaboration with {userName} has been canceled',
+  '操作已确认': 'Action confirmed',
+  '移动场景': 'Move Scene',
+  '暂无文件夹,请点击【新建文件夹】创建': 'No folders yet. Click [New Folder] to create one',
+  '已移动到目标文件夹': 'Moved to the target folder',
+  '请输入文件名称': 'Please enter a file name',
+  '附加产物': 'Additional Outputs',
+  '场景信息': 'Scene Information',
+  '生成 E57': 'Generate E57',
+  '拍摄设备:': 'Capture Device:',
+  '拍摄方式:': 'Capture Mode:',
+  '拍摄时间:': 'Capture Time:',
+  '拍摄点位:': 'Capture Points:',
+  '拍摄地点:': 'Capture Location:',
+  '计算完成:': 'Calculation Completed:',
+  '计算时长:': 'Calculation Duration:',
+  '场景大小:': 'Scene Size:',
+  '架站式': 'Tripod',
+  '打包中': 'Packaging',
+  '计算完成时间:{time}': 'Calculation completed at: {time}',
+  '过期时间:{time}': 'Expires: {time}',
+  '场景码': 'Scene Code',
+  'SN码': 'S/N Code',
+  '正常': 'Normal',
+  '计算失败': 'Calculation Failed',
+  '取消协助': 'Cancel Collaboration',
+  '批量操作': 'Batch Actions',
+  '已选择 {num} 项': '{num} item(s) selected',
+  '计算中/已封存的场景无法协作': 'Scenes being calculated or archived cannot be shared for collaboration',
+  '此场景将复制到相同文件夹中,该副本将占用您的相机云容量。复制过程中,请勿编辑场景。':
+    'This scene will be copied to the same folder. The copy will use your camera cloud storage. Do not edit the scene during copying.',
+  '四维深巡': '4D Deep Patrol',
+  '深智云平台': 'DeepAI Cloud Platform',
+  '请输入相机S/N码': 'Please enter the camera S/N code',
+  '成功': 'Success',
+  '前往支付': 'Proceed to Payment',
+  '专业会员': 'Professional Membership',
+  '高级会员': 'Premium Membership',
+  '个月': ' month(s)',
+  '可授权相机数量:': 'Authorized cameras:',
+  '确定要解除该相机的权益吗?': 'Are you sure you want to revoke membership from this camera?',
+  '选择会员类型': 'Select membership type',
+  '取消订阅': 'Cancel Subscription',
+  '自动续费': 'Auto-renewal',
+  '当前会员权益订阅服务已取消成功': 'Membership subscription canceled successfully',
+  '非强制消费。在每个续期日期前至少一天您可以随时在“账号信息” > “会员权益”  > “取消订阅”   中取消。方案将自动续期,直到您取消为止。':
+    'Not mandatory. You may cancel anytime at least one day before each renewal date under Account Info > Membership > Cancel Subscription. The plan auto-renews until you cancel.',
+  '如果您取消订阅,我们将无法再为您提供会员服务。您还想要取消订阅吗?':
+    'If you cancel, we will no longer be able to provide membership services. Do you still want to cancel?',
+  '无限空间': 'Unlimited Space',
+  '每台相机 {number} 个空间': '{number} spaces per camera',
+  '购买会员权益立享': 'Purchase membership to enjoy',
+  '专业特权': 'professional benefits',
+  '四维看看精选三脚架套装': '4DKanKan Selected Tripod Kit',
+  '同意': 'Agree',
+  '四维看看销售协议': '4DKanKan Sales Agreement',
+  '支付': 'Pay',
+  '四维全景制作': '4D Panorama Production',
+  '请填写收票地址': 'Please fill in the billing address',
+  '四维看看会员权益': '4DKanKan Membership Benefits',
+  '发票抬头': 'Invoice Title',
+  '暂无相关记录': 'No records found',
+  '收票地址': 'Billing Address',
+  '发票类型': 'Invoice Type',
+  '请输入开票金额': 'Please enter the invoice amount',
+  '可开票额度:{money}元': 'Available invoice amount: {money} CNY',
+  '支持图片格式为:JPG/PNG': 'Supported formats: JPG/PNG',
+  '推荐分辨率为:512*512px,<br/>允许JPG、PNG格式,小于1MB':
+    'Recommended resolution: 512×512px.<br/>JPG or PNG, under 1MB',
+  '推荐分辨率为:512*512px,允许JPG、PNG格式,小于1MB':
+    'Recommended resolution: 512×512px. JPG or PNG, under 1MB',
+  '技术规格': 'Technical Specifications',
+  '*由于受环境光、显示器色差、闪光等拍摄的影响,产品颜色上可能存在一定的彩色偏差,请见谅。':
+    '*Color may vary slightly due to ambient light, display differences, or flash. Thank you for your understanding.',
+  '注:本页面所列配置及参数均以实际上市产品为准,如有变更,恕不另行通知':
+    'Note: Specifications and parameters are subject to the actual product. Changes may occur without notice.',
+  '产品型号': 'Model',
+  '产品名称': 'Product Name',
+  '产品重量': 'Product Weight',
+  '云台类型': 'Head Type',
+  '脚架材质': 'Leg Material',
+  '脚管管径': 'Leg Tube Diameter',
+  '脚管节数': 'Leg Sections',
+  '安全负重': 'Load Capacity',
+  '收纳高度': 'Folded Height',
+  '低角度': 'Low Angle',
+  '展开高度': 'Extended Height',
+  '原厂标配': 'Standard Package',
+  '品牌型号': 'Brand / Model',
+  '脚管最小长度': 'Min Leg Length',
+  '脚管最大长度': 'Max Leg Length',
+  '脚管最短距离': 'Min Leg Spread',
+  '脚管最长距离': 'Max Leg Spread',
+  '收纳长度': 'Folded Length',
+  '材 料': 'Material',
+  '颜 色': 'Color',
+  '承担能力': 'Load Capacity',
+  '自 重': 'Weight',
+  '解锁类型': 'Lock Type',
+  '个人版': 'Personal',
+  '场景管理': 'Scene Management',
+  '点云场景': 'Point Cloud Scene',
+  '应用管理': 'App Management',
+  '全景看看': '4DKanKan Pro',
+  '设备和权益': 'Devices & Benefits',
+  '会员权益': 'Membership',
+  '订单管理': 'Orders',
+  '设备管理': 'Device Management',
+  '深度空间智能管理平台': 'Deep Spatial Intelligence Management Platform',
+  '数字孪生引擎 • 行业深度 • AI分析': 'Digital Twin Engine • Industry Depth • AI Analytics',
+  '扫码登录': 'Scan to Log In',
+  '暂无数据': 'No data',
+  '中国': 'China',
+  '弹窗标题': 'Dialog',
+  '提示': 'Notice',
+  '搜索': 'Search',
+  '输入': 'Enter',
+  '全部': 'All',
+  '我的上传': 'My Uploads',
+  '操作': 'Actions',
+  '内容': 'Content',
+  '取消': 'Cancel',
+  '确定': 'Confirm',
+  '修改昵称': 'Change Nickname',
+  '修改密码': 'Change Password',
+  '修改收货地址': 'Edit Shipping Address',
+  '账号设置': 'Account Settings',
+  '请选择省市区': 'Please select province, city, and district',
+  '省、市、区': 'Province, city, district',
+  '请输入详细地址': 'Please enter the detailed address',
+  '最多50个字': 'Maximum 50 characters',
+  '请输入收件人': 'Please enter the recipient name',
+  '最多10个字': 'Maximum 10 characters',
+  '请输入名称': 'Please enter a name',
+  '请输入手机号': 'Please enter a phone number',
+  '手机号必须11位': 'Phone number must be 11 digits',
+  '修改地址': 'Edit Address',
+  '我的权益': 'My Benefits',
+  '地区分布': 'Regional Distribution',
+  '查看更多': 'View More',
+  '功能更新': 'Feature Update',
+  '点位校准功能更新了支持全站仪控制点校准,来试试吧!':
+    'Point calibration now supports total station control points. Give it a try!',
+  '四维全景': '4D Panorama',
+  'VR小程序对接': 'VR Mini Program Integration',
+  'AI训练': 'AI Training',
+  '请输入商品名称或订单编号': 'Enter product name or order number',
+  '订单编号:{sn}': 'Order No.: {sn}',
+  '订单信息': 'Order Information',
+  '类型:': 'Type:',
+  '订单编号:': 'Order No.:',
+  '下单时间:': 'Order Time:',
+  '付款时间:': 'Payment Time:',
+  '内容:': 'Content:',
+  '开通时间:': 'Activation Time:',
+  '到期时间:': 'Expiration Time:',
+  '总价:': 'Total:',
+  '支付方式:': 'Payment Method:',
+  '订单状态:': 'Order Status:',
+  '请选择发票类型': 'Please select invoice type',
+  '请输入发票抬头': 'Please enter invoice title',
+  '请输入税务登记号': 'Please enter tax registration number',
+  '税务登记号必须18位': 'Tax registration number must be 18 characters',
+  '请输入联系电话': 'Please enter contact phone number',
+  '手机号格式错误': 'Invalid phone number format',
+  '请输入电子邮箱': 'Please enter email address',
+  '邮箱格式错误': 'Invalid email format',
+  '请输入注册地址': 'Please enter registered address',
+  '请输入注册电话': 'Please enter registered phone number',
+  '请输入开户银行': 'Please enter bank name',
+  '请输入银行账号': 'Please enter bank account number',
+  '银行账号格式错误': 'Invalid bank account format',
+  '联系电话': 'Contact Phone',
+  '增值税普票(电子发票)': 'VAT General Invoice (Electronic)',
+  '发票类型:': 'Invoice Type:',
+  '发票抬头:': 'Invoice Title:',
+  '税务登记号:': 'Tax Registration No.:',
+  '电子邮箱:': 'Email:',
+  '注册地址:': 'Registered Address:',
+  '注册电话:': 'Registered Phone:',
+  '开户银行:': 'Bank Name:',
+  '银行账号:': 'Bank Account:',
+  '联系电话:': 'Contact Phone:',
+  '增值税普票电子发票': 'VAT General Electronic Invoice',
+  '增值税专用发票': 'VAT Special Invoice',
+  '协作设置': 'Collaboration Settings',
+  '请输入S/N码搜索': 'Search by S/N code',
+  '请输入设备机身的S/N码': 'Enter the S/N code on the device',
+  '授权相机': 'Authorize Camera',
+  '请选择用户': 'Please select a user',
+  '暂无匹配用户': 'No matching users',
+  '绑定相机': 'Bind Camera',
+  '请输入重命名内容或选择用户!': 'Please enter a name or select a user',
+  '新建文件夹': 'New Folder',
+  '文件夹名称不能为空': 'Folder name cannot be empty',
+  '删除成功': 'Deleted successfully',
+  '请选择目标文件夹': 'Please select a target folder',
+  '重命名': 'Rename',
+  '请先阅读并同意用户协议': 'Please read and agree to the user agreement first',
+  '注册账号': 'Register Account',
+  '更多': 'More',
+  '带看': 'Live Tour',
+  '复制': 'Copy',
+  '生成 obj': 'Generate OBJ',
+  '生成obj': 'Generate OBJ',
+  '生成obj需要较长时间,请耐心等待': 'Generating OBJ may take a while. Please wait.',
+  '抱歉,您访问的页面出现了错误,请重新加载': 'Sorry, an error occurred. Please reload the page.',
+  '返回首页': 'Back to Home',
+  '粤ICP备14078495号': 'Guangdong ICP No. 14078495',
+  '粤公网安备 44049102496647号': 'Guangdong Public Security Record No. 44049102496647',
+  '推荐分辨率为:512*512px,大小不大于1MB': 'Recommended resolution: 512×512px, under 1MB',
+  '滑轮直径': 'Wheel Diameter',
+  '配 置': 'Configuration',
+  '产品尺寸': 'Product Dimensions',
+  '存储介质': 'Storage Medium',
+  '工作电压': 'Operating Voltage',
+  '工作电流': 'Operating Current',
+  '操作温度': 'Operating Temperature',
+  '保存温度': 'Storage Temperature',
+  '接口类型': 'Interface Type',
+  '速   度': 'Speed',
+  '操作系统': 'Operating System',
+  '专业拍摄脚架': 'Professional Camera Tripod',
+  '采用铝合金材质锻造,兼顾轻便性与可靠性': 'Forged aluminum alloy for lightweight strength and reliability',
+  '专业便携脚架细节': 'Professional Portable Tripod Details',
+  '专业脚架滑轮': 'Professional Tripod Dolly',
+  '稳固可靠 操作顺畅': 'Stable and reliable with smooth operation',
+  '专业球形云台': 'Professional Ball Head',
+  '脚管三段角度调节按钮': 'Three-position leg angle lock buttons',
+  '安全的中轴升降锁紧按钮': 'Secure center column lock button',
+  '防滑保暖致密海绵护手': 'Non-slip dense foam grip',
+  '可伸缩中轴挂钩稳定脚架': 'Retractable center column hook for stability',
+  '方便快捷的版扣脚管': 'Quick-release leg locks',
+  '三脚架型号:ET1004三脚架': 'Tripod Model: ET1004',
+  '云台型号:Y-10云台': 'Head Model: Y-10',
+  '脚轮型号:B900脚轮': 'Dolly Model: B900',
+  '服务:四维深时本地版专用': 'Service: For 4D Deep Time Local Edition',
+  '容量:128GB': 'Capacity: 128GB',
+  'USB3.1 Type-C 双U口': 'USB 3.1 Type-C dual USB ports',
+  '服务:四维深时专用': 'Service: For 4D Deep Time',
+  '容量:9700 mAh': 'Capacity: 9700 mAh',
+  '接口:Type -C': 'Interface: Type-C',
+  '离线包下载服务': 'Offline Package Download Service',
+  'VR 场景离线包支持本地离线打开,无需安装应用,浏览器直接访问。':
+    'VR scene offline packages open locally in the browser without installing an app.',
+  '小程序需完成与四维服务的对接,才能解锁 VR 全景展示能力,享受完整流畅的 VR 体验。':
+    'The mini program must integrate with 4D services to unlock VR panorama features for a smooth VR experience.',
+  '基于专业大模型技术,为客户提供专属 AI 模型训练服务。':
+    'Professional large-model technology for custom AI model training services.',
+  '更改昵称': 'Change Nickname',
+  '更改密码': 'Change Password',
+  '请输入昵称': 'Please enter a nickname',
+  '最多50个字符': 'Maximum 50 characters',
+  '昵称修改成功': 'Nickname updated successfully',
+  '验证码已发送': 'Verification code sent',
+  '地址修改成功': 'Address updated successfully',
+  '场景简介': 'Scene Description',
+  '请输入场景简介': 'Please enter a scene description',
+  '二维码Logo': 'QR Code Logo',
+  '地面logo': 'Floor Logo',
+  '上传并保存成功': 'Uploaded and saved successfully',
+  '图片已移除': 'Image removed',
+  '台': ' unit(s)',
+  '次': ' time(s)',
+  '个': '',
+  '验证码错误或已过期': 'Verification code is incorrect or expired',
+  '请输入用户账号': 'Please enter user account',
+  ' 空间': ' spaces',
+  '暂不支持降级套餐': 'Downgrading is not supported',
+  '您可进行续费或升级套餐': 'You may renew or upgrade your plan',
+  '表示相机的点数超过了10万': 'Camera points exceed 100,000',
+  '必须输入需迁相机所绑定用户的验证码': 'Verification code of the source camera account is required',
+  '必须输入目标相机所绑定用户的验证码': 'Verification code of the target camera account is required',
+  '请删除其他场景或扩容,以保证足够的容量。': 'Please delete other scenes or expand storage to ensure sufficient capacity.',
+};

+ 51 - 0
scripts/patch-en-chinese.cjs

@@ -0,0 +1,51 @@
+/**
+ * 将 en.json 中仍为中文的条目按 zh.json 对应值翻译为英文
+ * 用法: node scripts/patch-en-chinese.cjs
+ */
+const fs = require('fs');
+const root = 'd:/project/2026项目/DeepAICloud';
+const zhPath = `${root}/src/locales/zh.json`;
+const enPath = `${root}/src/locales/en.json`;
+const zhToEnManual = require('./en-translations-manual.cjs');
+
+const zh = JSON.parse(fs.readFileSync(zhPath, 'utf8'));
+const en = JSON.parse(fs.readFileSync(enPath, 'utf8'));
+
+let patched = 0;
+function patchByZh(zhPart, enPart) {
+  if (typeof zhPart === 'string' && typeof enPart === 'string') {
+    if (/[\u4e00-\u9fff]/.test(enPart) && Object.prototype.hasOwnProperty.call(zhToEnManual, zhPart)) {
+      enPart = zhToEnManual[zhPart];
+      patched++;
+    }
+    return enPart;
+  }
+  if (Array.isArray(zhPart) && Array.isArray(enPart)) {
+    return zhPart.map((item, i) => patchByZh(item, enPart[i]));
+  }
+  if (zhPart && enPart && typeof zhPart === 'object' && typeof enPart === 'object') {
+    for (const key of Object.keys(zhPart)) {
+      enPart[key] = patchByZh(zhPart[key], enPart[key]);
+    }
+    return enPart;
+  }
+  return enPart;
+}
+
+patchByZh(zh, en);
+fs.writeFileSync(enPath, `${JSON.stringify(en, null, 2)}\n`);
+
+const remaining = [];
+function countRemaining(zhPart, enPart, path = '') {
+  if (typeof zhPart === 'string' && typeof enPart === 'string') {
+    if (/[\u4e00-\u9fff]/.test(enPart)) remaining.push({ path, zh: zhPart, en: enPart });
+    return;
+  }
+  if (zhPart && enPart && typeof zhPart === 'object' && !Array.isArray(zhPart)) {
+    for (const key of Object.keys(zhPart)) {
+      countRemaining(zhPart[key], enPart[key], path ? `${path}.${key}` : key);
+    }
+  }
+}
+countRemaining(zh, en);
+console.log(`Patched ${patched} strings. Remaining Chinese in en.json: ${remaining.length}`);

+ 8 - 6
src/api/mall/index.js

@@ -22,9 +22,11 @@ export const serviceConfigList = (params) =>
 export const userSerciceList = (params) =>
 export const userSerciceList = (params) =>
   request.get(Api.userSerciceList, { params });
   request.get(Api.userSerciceList, { params });
 export const userSerciceBuy = (params) =>
 export const userSerciceBuy = (params) =>
-  request.post(Api.userSerciceBuy, params, {
-    headers: {
-      // @ts-ignore
-      ignoreCancelToken: true,
-    },
-  });
+  request.get(Api.userSerciceBuy, { params });
+// export const userSerciceBuy = (params) =>
+//   request.post(Api.userSerciceBuy, params, {
+//     headers: {
+//       // @ts-ignore
+//       ignoreCancelToken: true,
+//     },
+//   });

+ 7 - 0
src/api/scene/index.js

@@ -25,6 +25,7 @@ const Api = {
   assumeRole: 'cloud-acl/sts/assumeRole',
   assumeRole: 'cloud-acl/sts/assumeRole',
   copyScene: 'ucenter/user/scene/copyScene',
   copyScene: 'ucenter/user/scene/copyScene',
   sceneDetail: 'ucenter/szy/sceneDetail',
   sceneDetail: 'ucenter/szy/sceneDetail',
+  getTakeLookRoomId: 'ucenter/user/scene/getTakeLookRoomId',
   buildObj: 'ucenter/szy/buildObj',
   buildObj: 'ucenter/szy/buildObj',
   sceneResourceList: 'ucenter/user/scene/cooperation/sceneResourceList',
   sceneResourceList: 'ucenter/user/scene/cooperation/sceneResourceList',
   saveBatch: 'ucenter/user/scene/cooperation/saveBatch',
   saveBatch: 'ucenter/user/scene/cooperation/saveBatch',
@@ -141,6 +142,12 @@ export const sceneDetail = (params) =>
       ignoreCancelToken: true,
       ignoreCancelToken: true,
     },
     },
   });
   });
+export const getTakeLookRoomId = (params) =>
+  request.get(Api.getTakeLookRoomId, {
+    params,
+    isReturnNativeResponse: true,
+    allowLoading: false,
+  });
 export const buildSceneObj = (params) =>
 export const buildSceneObj = (params) =>
   request.post(Api.buildObj, params, {
   request.post(Api.buildObj, params, {
     headers: {
     headers: {

+ 6 - 0
src/assets/style/index.less

@@ -10,6 +10,12 @@
   line-height:normal;
   line-height:normal;
 }
 }
 
 
+html,
+body,
+#app {
+  min-width: 1366px;
+}
+
 .title {
 .title {
   color: @color;
   color: @color;
   font-weight: normal;
   font-weight: normal;

+ 36 - 37
src/components/AddressModal.vue

@@ -1,10 +1,10 @@
 <template>
 <template>
   <a-modal
   <a-modal
     v-model:open="visible"
     v-model:open="visible"
-    title="修改地址"
+    :title="t('information.editAddressLinkText')"
     :width="600"
     :width="600"
-    ok-text="确定"
-    cancel-text="取消"
+    :ok-text="t('common.comfirm')"
+    :cancel-text="t('common.cancel')"
     @ok="handleSubmit"
     @ok="handleSubmit"
   >
   >
     <a-form
     <a-form
@@ -13,74 +13,65 @@
       :label-col="{ style: { width: '120px' } }"
       :label-col="{ style: { width: '120px' } }"
       :wrapper-col="{ span: 18 }"
       :wrapper-col="{ span: 18 }"
     >
     >
-      <a-form-item label="国家/地区" name="country">
+      <a-form-item :label="t('information.editAddress.country')" name="country">
         <a-select size="large" v-model:value="form.country">
         <a-select size="large" v-model:value="form.country">
-          <a-select-option value="CN">中国</a-select-option>
+          <a-select-option value="CN">{{ t('common.china') }}</a-select-option>
         </a-select>
         </a-select>
       </a-form-item>
       </a-form-item>
 
 
       <a-form-item
       <a-form-item
-        label="所在地区"
+        :label="t('information.editAddress.area')"
         name="area"
         name="area"
-        :rules="[{ required: true, message: '请选择省市区' }]"
+        :rules="[{ required: true, message: t('information.editAddress.selectAreaRequired') }]"
       >
       >
         <a-cascader
         <a-cascader
           size="large"
           size="large"
           v-model:value="form.area"
           v-model:value="form.area"
           :options="regionData"
           :options="regionData"
-          placeholder="省、市、区"
+          :placeholder="t('information.editAddress.areaPlaceholder')"
           style="width:100%"
           style="width:100%"
         />
         />
       </a-form-item>
       </a-form-item>
 
 
       <a-form-item
       <a-form-item
-        label="详细地址"
+        :label="t('information.editAddress.addressDetail')"
         name="shipAddress"
         name="shipAddress"
-        :rules="[
-          { required: true, message: '请输入详细地址' },
-          { max: 50, message: '最多50个字' }
-        ]"
+        :rules="formRules.shipAddress"
       >
       >
         <a-input
         <a-input
           size="large"
           size="large"
           v-model:value="form.shipAddress"
           v-model:value="form.shipAddress"
           show-count
           show-count
           maxlength="50"
           maxlength="50"
-          placeholder="请输入详细地址"
+          :placeholder="t('information.editAddress.addressRequired')"
         />
         />
       </a-form-item>
       </a-form-item>
 
 
       <a-form-item
       <a-form-item
-        label="收件人"
+        :label="t('information.editAddress.receiver')"
         name="shipName"
         name="shipName"
-        :rules="[
-          { required: true, message: '请输入收件人' },
-          { max: 10, message: '最多10个字' }
-        ]"
+        :rules="formRules.shipName"
       >
       >
         <a-input
         <a-input
           size="large"
           size="large"
           v-model:value="form.shipName"
           v-model:value="form.shipName"
           show-count
           show-count
           maxlength="10"
           maxlength="10"
-          placeholder="请输入名称"
+          :placeholder="t('information.editAddress.namePlaceholder')"
         />
         />
       </a-form-item>
       </a-form-item>
 
 
       <a-form-item
       <a-form-item
-        label="手机号码"
+        :label="t('information.editAddress.receiverPhone')"
         name="shipMobile"
         name="shipMobile"
-        :rules="[
-          { required: true, message: '请输入手机号' },
-          { len: 11, message: '手机号必须11位' }
-        ]"
+        :rules="formRules.shipMobile"
       >
       >
         <a-input
         <a-input
           size="large"
           size="large"
           v-model:value="form.shipMobile"
           v-model:value="form.shipMobile"
           show-count
           show-count
           maxlength="11"
           maxlength="11"
-          placeholder="请输入手机号码"
+          :placeholder="t('login.phonePlaceholder')"
         />
         />
       </a-form-item>
       </a-form-item>
     </a-form>
     </a-form>
@@ -89,16 +80,31 @@
 
 
 <script setup>
 <script setup>
 import { ref, watch, computed } from 'vue'
 import { ref, watch, computed } from 'vue'
-// 导入三级省市区数据
+import { useI18n } from 'vue-i18n'
 import { useCartStore, useUserStore } from "@/stores";
 import { useCartStore, useUserStore } from "@/stores";
 import { regionData } from 'element-china-area-data'
 import { regionData } from 'element-china-area-data'
 import { updateAddress } from "@/api/login/index.js";
 import { updateAddress } from "@/api/login/index.js";
+
+const { t } = useI18n();
 const cart = useCartStore();
 const cart = useCartStore();
 const receiverInfo = computed(() => cart.greceiverInfo);
 const receiverInfo = computed(() => cart.greceiverInfo);
 
 
-// regionData 结构完全匹配 a-cascader 默认字段 label / value / children
+const formRules = computed(() => ({
+  shipAddress: [
+    { required: true, message: t('information.editAddress.addressRequired'), trigger: 'blur' },
+    { max: 50, message: t('information.editAddress.maxChars50'), trigger: 'blur' },
+  ],
+  shipName: [
+    { required: true, message: t('information.editAddress.receiverRequired'), trigger: 'blur' },
+    { max: 10, message: t('information.editAddress.maxChars10'), trigger: 'blur' },
+  ],
+  shipMobile: [
+    { required: true, message: t('information.editAddress.phoneRequired'), trigger: 'blur' },
+    { len: 11, message: t('information.editAddress.phoneLen11'), trigger: 'blur' },
+  ],
+}));
+
 const props = defineProps({
 const props = defineProps({
-  // 弹窗显示隐藏 v-model:open
   open: {
   open: {
     type: Boolean,
     type: Boolean,
     default: false
     default: false
@@ -107,13 +113,10 @@ const props = defineProps({
 
 
 const emit = defineEmits(['update:open', 'confirm'])
 const emit = defineEmits(['update:open', 'confirm'])
 
 
-// 表单ref
 const addrRef = ref(null)
 const addrRef = ref(null)
 
 
-// 内部表单对象
 const form = ref(receiverInfo.value || {})
 const form = ref(receiverInfo.value || {})
 
 
-// 监听外部传入地址,弹窗打开回填数据
 watch(
 watch(
   () => receiverInfo.value,
   () => receiverInfo.value,
   (val) => {
   (val) => {
@@ -123,12 +126,10 @@ watch(
   { deep: true, immediate: true }
   { deep: true, immediate: true }
 )
 )
 
 
-// 弹窗开关同步父组件
 watch(
 watch(
   () => props.open,
   () => props.open,
   (val) => {
   (val) => {
     if (!val) {
     if (!val) {
-      // 关闭弹窗清空校验
       addrRef.value?.clearValidate()
       addrRef.value?.clearValidate()
     }
     }
   }
   }
@@ -143,7 +144,6 @@ const visible = computed({
   }
   }
 })
 })
 
 
-// 点击确定校验并抛出数据
 const handleSubmit = async () => {
 const handleSubmit = async () => {
   await addrRef.value.validate()
   await addrRef.value.validate()
   await updateAddress({
   await updateAddress({
@@ -154,7 +154,6 @@ const handleSubmit = async () => {
   });
   });
   console.log('form', form);
   console.log('form', form);
   cart.getReceiver();
   cart.getReceiver();
-  // 校验通过抛出数据给父组件
   emit('confirm', { ...form.value })
   emit('confirm', { ...form.value })
 }
 }
-</script>
+</script>

+ 12 - 6
src/components/EmptyData/index.vue

@@ -1,23 +1,29 @@
 <template>
 <template>
   <div class="empty-wrap">
   <div class="empty-wrap">
-    <img :src="imgUrl" alt="暂无数据" class="empty-img" />
-    <p class="empty-text">{{ text }}</p>
+    <img :src="imgUrl" :alt="displayText" class="empty-img" />
+    <p class="empty-text">{{ displayText }}</p>
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
+import { computed } from "vue";
+import { useI18n } from "vue-i18n";
 import emptyImg from "@/assets/image/empty.png";
 import emptyImg from "@/assets/image/empty.png";
-// 接收外部传参
+
+const { t } = useI18n();
+
 const props = defineProps({
 const props = defineProps({
   imgUrl: {
   imgUrl: {
     type: String,
     type: String,
-    default: emptyImg, // 默认空图地址
+    default: emptyImg,
   },
   },
   text: {
   text: {
     type: String,
     type: String,
-    default: "暂无数据", // 默认文字
+    default: "",
   },
   },
 });
 });
+
+const displayText = computed(() => props.text || t("common.noData"));
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>
@@ -40,4 +46,4 @@ const props = defineProps({
   font-size: 14px;
   font-size: 14px;
   margin: 0;
   margin: 0;
 }
 }
-</style>
+</style>

+ 15 - 9
src/components/myModal.vue

@@ -1,10 +1,12 @@
 <template>
 <template>
   <a-modal
   <a-modal
     v-model:open="visible"
     v-model:open="visible"
-    title=""
+    :title="displayTitle"
     :width="width"
     :width="width"
     :maskClosable="maskClosable"
     :maskClosable="maskClosable"
     :confirm-loading="loading"
     :confirm-loading="loading"
+    :ok-text="displayOkText"
+    :cancel-text="displayCancelText"
     :footer="null"
     :footer="null"
     @cancel="handleCancel"
     @cancel="handleCancel"
     @ok="handleOk"
     @ok="handleOk"
@@ -27,8 +29,11 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { computed, defineEmits, defineProps } from "vue";
 import { computed, defineEmits, defineProps } from "vue";
+import { useI18n } from "vue-i18n";
 import { CloseOutlined } from "@ant-design/icons-vue";
 import { CloseOutlined } from "@ant-design/icons-vue";
-// props定义
+
+const { t } = useI18n();
+
 const props = defineProps({
 const props = defineProps({
   modelValue: {
   modelValue: {
     type: Boolean,
     type: Boolean,
@@ -36,7 +41,7 @@ const props = defineProps({
   },
   },
   title: {
   title: {
     type: String,
     type: String,
-    default: "弹窗标题",
+    default: "",
   },
   },
   width: {
   width: {
     type: [String, Number],
     type: [String, Number],
@@ -56,15 +61,18 @@ const props = defineProps({
   },
   },
   okText: {
   okText: {
     type: String,
     type: String,
-    default: "确定",
+    default: "",
   },
   },
   cancelText: {
   cancelText: {
     type: String,
     type: String,
-    default: "取消",
+    default: "",
   },
   },
 });
 });
 
 
-// 绑定v-model
+const displayTitle = computed(() => props.title);
+const displayOkText = computed(() => props.okText || t("common.comfirm"));
+const displayCancelText = computed(() => props.cancelText || t("common.cancel"));
+
 const emit = defineEmits(["update:modelValue", "ok", "cancel"]);
 const emit = defineEmits(["update:modelValue", "ok", "cancel"]);
 const visible = computed({
 const visible = computed({
   get() {
   get() {
@@ -75,14 +83,12 @@ const visible = computed({
   },
   },
 });
 });
 
 
-// 取消事件
 const handleCancel = () => {
 const handleCancel = () => {
   emit("cancel");
   emit("cancel");
   visible.value = false;
   visible.value = false;
   console.log("模态框已关闭");
   console.log("模态框已关闭");
 };
 };
 
 
-// 确定事件
 const handleOk = () => {
 const handleOk = () => {
   emit("ok");
   emit("ok");
 };
 };
@@ -105,4 +111,4 @@ const handleOk = () => {
     text-decoration: none;
     text-decoration: none;
   }
   }
 }
 }
-</style>
+</style>

+ 6 - 6
src/layouts/footer.vue

@@ -1,7 +1,7 @@
 <template>
 <template>
   <div >
   <div >
     <div class="footer" :style="{ position: 'relative' }">
     <div class="footer" :style="{ position: 'relative' }">
-      <div class="layout container">
+      <div class="container layout">
         <div class="infos clear">
         <div class="infos clear">
           <div class="infos-contact">
           <div class="infos-contact">
             <img 
             <img 
@@ -45,7 +45,7 @@
             />
             />
           </ul>
           </ul>
         </div>
         </div>
-        <div class="relevant flex justify-between">
+        <div class="flex justify-between relevant">
           <p class="relevant-1">
           <p class="relevant-1">
             Copyright © 2022 4DAGE Co., Ltd. All rights reserved.
             Copyright © 2022 4DAGE Co., Ltd. All rights reserved.
           </p>
           </p>
@@ -54,7 +54,7 @@
             <a
             <a
               target="_blank"
               target="_blank"
               href="https://beian.miit.gov.cn/"
               href="https://beian.miit.gov.cn/"
-              class="a_class flex items-center"
+              class="flex items-center a_class"
             >
             >
               <img src="@/assets/image/baicon.png" alt="" />
               <img src="@/assets/image/baicon.png" alt="" />
               粤ICP备14078495号
               粤ICP备14078495号
@@ -64,7 +64,7 @@
             <a
             <a
               target="_blank"
               target="_blank"
               href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=44049102496647"
               href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=44049102496647"
-              class="a_class flex items-center"
+              class="flex items-center a_class"
             >
             >
               <img src="@/assets/image/baicon.png" alt="" />
               <img src="@/assets/image/baicon.png" alt="" />
               粤公网安备 44049102496647号
               粤公网安备 44049102496647号
@@ -99,7 +99,7 @@ const userStore = useUserStore();
 
 
 
 
 // 响应式数据
 // 响应式数据
-const pathname = ref(window.location.pathname)
+const pathname = ref(import.meta.env.VITE_PATH || window.location.pathname)
 
 
 // 映射Vuex状态(替代mapState)
 // 映射Vuex状态(替代mapState)
 const language = computed(() => userStore.lang)
 const language = computed(() => userStore.lang)
@@ -163,7 +163,7 @@ const infosList = computed(() => [
         link: "service/app/pro"
         link: "service/app/pro"
       },
       },
       {
       {
-        text: t("service.help"),
+        text: t("header.help"),
         link: "service/use/help"
         link: "service/use/help"
       },
       },
       {
       {

+ 8 - 8
src/layouts/header.vue

@@ -40,25 +40,25 @@ const handleMenuClick = (e) => {
 };
 };
 </script>
 </script>
 <template>
 <template>
-  <div class="h-21 bg-white flex items-center justify-between px-10">
+  <div class="flex items-center justify-between px-10 bg-white h-21">
     <div class="haeder-left">
     <div class="haeder-left">
       <span v-if="route.name == 'sceneomore'" class="haeder-left-name">
       <span v-if="route.name == 'sceneomore'" class="haeder-left-name">
-        <span class="font-route cursor-pointer" @click="$router.push(type == 'laser'?'/sceneobj': '/scenemesh')">{{type == 'laser'?$t('scene.obj'):$t('scene.mesh')}}</span>
+        <span class="cursor-pointer font-route" @click="$router.push(type == 'laser'?'/sceneobj': '/scenemesh')">{{type == 'laser'?$t('scene.obj'):$t('scene.mesh')}}</span>
         <span class="font-fh">/</span>
         <span class="font-fh">/</span>
-        <span class="font-name">{{type}}</span>
+        <span class="font-name">{{$t('scene.moreAction')}}</span>
       </span>
       </span>
     </div>
     </div>
     <div class="flex items-center gap-4">
     <div class="flex items-center gap-4">
-      <div class="haeder-right-item cursor-pointer">
+      <div class="cursor-pointer haeder-right-item">
         <BellOutlined />
         <BellOutlined />
       </div>
       </div>
 
 
-      <div class="haeder-right-item cursor-pointer">
+      <div class="cursor-pointer haeder-right-item">
         <QuestionCircleOutlined />
         <QuestionCircleOutlined />
       </div>
       </div>
 
 
       <div
       <div
-        class="haeder-right-item cursor-pointer haeder-right-head"
+        class="cursor-pointer haeder-right-item haeder-right-head"
         :class="{ vip: userStore.incrementNum }"
         :class="{ vip: userStore.incrementNum }"
         :style="{ 'background-image': `url(${userStore.head})` }"
         :style="{ 'background-image': `url(${userStore.head})` }"
         @click="$router.push('/information')"
         @click="$router.push('/information')"
@@ -73,8 +73,8 @@ const handleMenuClick = (e) => {
         </div>
         </div>
         <template #overlay>
         <template #overlay>
           <a-menu @click="handleMenuClick">
           <a-menu @click="handleMenuClick">
-            <a-menu-item key="1">我的账户 </a-menu-item>
-            <a-menu-item key="2">退出登录 </a-menu-item>
+            <a-menu-item key="1">{{ t('mall.myAccount') }}</a-menu-item>
+            <a-menu-item key="2">{{ t('mall.logOut') }}</a-menu-item>
           </a-menu>
           </a-menu>
         </template>
         </template>
       </a-dropdown>
       </a-dropdown>

+ 70 - 25
src/layouts/menu.vue

@@ -5,7 +5,7 @@
         <h2 class="text-xl font-bold text-blue-600">
         <h2 class="text-xl font-bold text-blue-600">
           <img src="@/assets/image/menu/logo.png" style="height: 38px" />
           <img src="@/assets/image/menu/logo.png" style="height: 38px" />
         </h2>
         </h2>
-        <div class="logoTips">个人版</div>
+        <div class="logoTips">{{ t('menu.personalEdition') }}</div>
       </div>
       </div>
       <div class="flex-1 overflow-y-auto">
       <div class="flex-1 overflow-y-auto">
         <a-menu
         <a-menu
@@ -24,18 +24,42 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, watch, h, onMounted } from 'vue';
+import { ref, watch, h, onMounted, computed } from 'vue';
 import { useRouter, useRoute } from 'vue-router'
 import { useRouter, useRoute } from 'vue-router'
+import { useI18n } from 'vue-i18n'
+import { useUserStore } from '@/stores'
 import { userSerciceList } from '@/api/mall'
 import { userSerciceList } from '@/api/mall'
-// import { hasPanoramaUserService } from '@/views/mall/data.ts'
 
 
+const { t } = useI18n()
 const router = useRouter()
 const router = useRouter()
 const route = useRoute()
 const route = useRoute()
+const userStore = useUserStore()
+
+/** 拼接第三方/相对路径为完整 URL */
+const resolveLink = (link) => {
+  if (!link) return ''
+  if (/^https?:\/\//i.test(link)) return link
+  const base = (import.meta.env.VITE_PATH || window.location.origin).replace(/\/$/, '')
+  const path = link.startsWith('/') ? link : `/${link}`
+  return `${base}${path}`
+}
+
+/** 在菜单树中按 key 查找项(含 link 配置) */
+const findMenuItem = (items, key) => {
+  for (const item of items) {
+    if (item.key === key) return item
+    if (item.children?.length) {
+      const found = findMenuItem(item.children, key)
+      if (found) return found
+    }
+  }
+  return null
+}
 
 
 // 路由选中key
 // 路由选中key
 const selectedKeys = ref([route.path])
 const selectedKeys = ref([route.path])
 
 
-const openKeys = ref(['scene', 'equityDevice']);
+const openKeys = ref(['scene', 'equityDevice', 'app']);
 
 
 // ========== 导入所有菜单图片(替换成你自己的图片路径)==========
 // ========== 导入所有菜单图片(替换成你自己的图片路径)==========
 // 首页
 // 首页
@@ -75,11 +99,15 @@ const renderIcon = (normalImg, activeImg, targetKey) => {
   })
   })
 }
 }
 
 
-// 菜单配置:每一项新增 iconNormal / iconActive,icon使用渲染函数
-const buildMenuItems = () => [
+// 菜单配置:key 为内部路由(/xxx);link 为第三方/外链(优先于 key 跳转)
+const buildMenuItems = () => {
+  const token = encodeURIComponent(userStore.getToken || '')
+  const lang = userStore.lang || 'zh'
+
+  return [
   {
   {
     key: '/home',
     key: '/home',
-    label: '首页',
+    label: t('menu.home'),
     iconNormal: homeNormal,
     iconNormal: homeNormal,
     iconActive: homeActive,
     iconActive: homeActive,
     icon: renderIcon(homeNormal, homeActive, '/home'),
     icon: renderIcon(homeNormal, homeActive, '/home'),
@@ -87,18 +115,18 @@ const buildMenuItems = () => [
   },
   },
   {
   {
     key: 'scene',
     key: 'scene',
-    label: '场景管理',
+    label: t('menu.sceneManage'),
     children: [
     children: [
       {
       {
         key: '/sceneobj',
         key: '/sceneobj',
-        label: '点云场景',
+        label: t('menu.pointCloudScene'),
         iconNormal: sceneObjNormal,
         iconNormal: sceneObjNormal,
         iconActive: sceneObjActive,
         iconActive: sceneObjActive,
         icon: renderIcon(sceneObjNormal, sceneObjActive, '/sceneobj'),
         icon: renderIcon(sceneObjNormal, sceneObjActive, '/sceneobj'),
       },
       },
       {
       {
         key: '/scenemesh',
         key: '/scenemesh',
-        label: 'Mesh场景',
+        label: t('menu.meshScene'),
         iconNormal: meshNormal,
         iconNormal: meshNormal,
         iconActive: meshActive,
         iconActive: meshActive,
         icon: renderIcon(meshNormal, meshActive, '/scenemesh'),
         icon: renderIcon(meshNormal, meshActive, '/scenemesh'),
@@ -107,38 +135,41 @@ const buildMenuItems = () => [
   },
   },
   {
   {
     key: 'app',
     key: 'app',
-    label: '应用管理',
+    label: t('menu.appManage'),
     children: [
     children: [
       {
       {
-        key: '/appProduct',
-        label: '全景看看',
+        // 外链菜单用独立 key,避免 router.push 到不存在的路由
+        key: 'app:panorama',
+        label: t('menu.panoramaApp'),
+        link: `/panorama/material.html?token=${token}&lang=${lang}`,
+        openInNewTab: true,
         iconNormal: appNormal,
         iconNormal: appNormal,
         iconActive: appActive,
         iconActive: appActive,
-        icon: renderIcon(appNormal, appActive, '/appProduct'),
+        icon: renderIcon(appNormal, appActive, 'app:panorama'),
       },
       },
     ],
     ],
   },
   },
   {
   {
     key: 'equityDevice',
     key: 'equityDevice',
-    label: '设备和权益',
+    label: t('menu.deviceAndEquity'),
     children: [
     children: [
       {
       {
         key: '/equity',
         key: '/equity',
-        label: '会员权益',
+        label: t('menu.memberEquity'),
         iconNormal: vipNormal,
         iconNormal: vipNormal,
         iconActive: vipActive,
         iconActive: vipActive,
         icon: renderIcon(vipNormal, vipActive, '/equity'),
         icon: renderIcon(vipNormal, vipActive, '/equity'),
       },
       },
       {
       {
         key: '/order/list',
         key: '/order/list',
-        label: '订单管理',
+        label: t('menu.orderManage'),
         iconNormal: orderNormal,
         iconNormal: orderNormal,
         iconActive: orderActive,
         iconActive: orderActive,
         icon: renderIcon(orderNormal, orderActive, '/order/list'),
         icon: renderIcon(orderNormal, orderActive, '/order/list'),
       },
       },
       {
       {
         key: '/device',
         key: '/device',
-        label: '设备管理',
+        label: t('menu.deviceManage'),
         iconNormal: deviceNormal,
         iconNormal: deviceNormal,
         iconActive: deviceActive,
         iconActive: deviceActive,
         icon: renderIcon(deviceNormal, deviceActive, '/device'),
         icon: renderIcon(deviceNormal, deviceActive, '/device'),
@@ -146,27 +177,41 @@ const buildMenuItems = () => [
     ],
     ],
   }
   }
 ];
 ];
+}
+
+const showAppMenu = ref(false);
 
 
-const menuItems = ref(buildMenuItems().filter((item) => item.key !== 'app'));
+const menuItems = computed(() => {
+  const items = buildMenuItems();
+  return showAppMenu.value ? items : items.filter((item) => item.key !== 'app');
+});
 
 
 const loadMenuItems = async () => {
 const loadMenuItems = async () => {
   try {
   try {
     const userList = await userSerciceList();
     const userList = await userSerciceList();
     console.log('userSerciceList', userList)
     console.log('userSerciceList', userList)
     const isApp = userList.find(item => item.id == 1)
     const isApp = userList.find(item => item.id == 1)
-    menuItems.value = isApp
-      ? buildMenuItems()
-      : buildMenuItems().filter((item) => item.key !== 'app');
+    showAppMenu.value = !!isApp
   } catch {
   } catch {
-    menuItems.value = buildMenuItems().filter((item) => item.key !== 'app');
+    showAppMenu.value = false
   }
   }
 };
 };
 
 
 onMounted(loadMenuItems);
 onMounted(loadMenuItems);
 
 
-// 菜单点击跳转
+// 菜单点击:link 优先走外链,否则 key 以 / 开头走 vue-router
 const handleClick = ({ key }) => {
 const handleClick = ({ key }) => {
-  // 只跳转路由key(带/),父分组不跳转
+  const item = findMenuItem(menuItems.value, key)
+  if (item?.link) {
+    const url = resolveLink(item.link)
+    if (item.openInNewTab !== false) {
+      window.open(url, '_blank', 'noopener,noreferrer')
+    } else {
+      window.location.href = url
+    }
+    selectedKeys.value = [key]
+    return
+  }
   if (key.startsWith('/')) {
   if (key.startsWith('/')) {
     router.push(key)
     router.push(key)
   }
   }

+ 1 - 0
src/stores/user.js

@@ -76,6 +76,7 @@ export const useUserStore = defineStore('user', {
           this[key] = data[key]
           this[key] = data[key]
         })
         })
       }
       }
+      return data
     },
     },
     async updateInfo (nickName) {
     async updateInfo (nickName) {
       await updateUserDetail({email: this.email, nickName: nickName, isNotice: 1})
       await updateUserDetail({email: this.email, nickName: nickName, isNotice: 1})

+ 2 - 2
src/views/404.vue

@@ -2,8 +2,8 @@
   <div class="layout-404">
   <div class="layout-404">
     <div class="_404">
     <div class="_404">
       <img :src="`${$cdn}images/404.png`" alt="">
       <img :src="`${$cdn}images/404.png`" alt="">
-      <p>抱歉,您访问的页面出现了错误,请重新加载</p>
-      <div @click="$router.push({name:'home'})">返回首页</div>
+      <p>{{ $t('page404.message') }}</p>
+      <div @click="$router.push({name:'home'})">{{ $t('page404.backHome') }}</div>
     </div>
     </div>
   </div>
   </div>
 </template>
 </template>

+ 11 - 6
src/views/account/components/header.vue

@@ -1,16 +1,21 @@
 <script setup>
 <script setup>
-import { ref, h } from 'vue'
+import { ref, h, computed } from 'vue'
 import { useUserStore } from "@/stores";
 import { useUserStore } from "@/stores";
-import { LOCALES } from "@/locales/index";
+import { LOCALES, changeLang } from "@/locales/index";
 import { GlobalOutlined, CaretDownFilled, CaretUpFilled } from "@ant-design/icons-vue";
 import { GlobalOutlined, CaretDownFilled, CaretUpFilled } from "@ant-design/icons-vue";
 const userStore = useUserStore();
 const userStore = useUserStore();
-const language = ref(userStore.lang || 'zh');
+const language = computed(() => userStore.lang || "zh");
+const selectedKeys = computed(() => [language.value]);
 const visible = ref(false);
 const visible = ref(false);
+const handleMenuClick = (item) => {
+  console.log(item);
+  changeLang(item.key);
+};
 </script>
 </script>
 <template>
 <template>
-  <div class="account-header flex items-center justify-between h-20">
+  <div class="flex items-center justify-between h-20 account-header">
     <img :src="language === 'en'?`${$cdn}mobile/logo/logo-en.png`:`${$cdn}mobile/logo/logo-cn.png`" alt>
     <img :src="language === 'en'?`${$cdn}mobile/logo/logo-en.png`:`${$cdn}mobile/logo/logo-cn.png`" alt>
-    <div class="headerRight flex items-center">
+    <div class="flex items-center headerRight">
       <a-dropdown v-model:open="visible" arrow placement="bottom">
       <a-dropdown v-model:open="visible" arrow placement="bottom">
         <div class="lang">
         <div class="lang">
           <GlobalOutlined style="font-size: 18px; margin-right: 8px;" />
           <GlobalOutlined style="font-size: 18px; margin-right: 8px;" />
@@ -18,7 +23,7 @@ const visible = ref(false);
           <CaretUpFilled v-else />
           <CaretUpFilled v-else />
         </div>
         </div>
         <template #overlay>
         <template #overlay>
-          <a-menu @click="handleMenuClick">
+          <a-menu @click="handleMenuClick" v-model:selectedKeys="selectedKeys">
             <a-menu-item v-for="item in LOCALES" :key="item.value">{{ item.label }} </a-menu-item>
             <a-menu-item v-for="item in LOCALES" :key="item.value">{{ item.label }} </a-menu-item>
           </a-menu>
           </a-menu>
         </template>
         </template>

+ 6 - 4
src/views/account/index.vue

@@ -1,7 +1,9 @@
 <script setup>
 <script setup>
-import { ref, h } from "vue";
 import loginHeader from "./components/header.vue";
 import loginHeader from "./components/header.vue";
 import { useRouter, useRoute } from 'vue-router'
 import { useRouter, useRoute } from 'vue-router'
+import { useI18n } from "vue-i18n";
+
+const { t } = useI18n();
 const router = useRouter()
 const router = useRouter()
 const route = useRoute()
 const route = useRoute()
 console.log(route)
 console.log(route)
@@ -21,15 +23,15 @@ const toOtherLogin = () => {
     <div class="account-container container mt-20">
     <div class="account-container container mt-20">
       <div class="loginContentdiv">
       <div class="loginContentdiv">
         <div class="loginTitle" style="top: 20%">
         <div class="loginTitle" style="top: 20%">
-          <div class="name">深度空间智能管理平台</div>
-          <div class="text">数字孪生引擎 • 行业深度 • AI分析</div>
+          <div class="name">{{ t('accountPage.platformDesc') }}</div>
+          <div class="text">{{ t('accountPage.platformSubDesc') }}</div>
         </div>
         </div>
         <div class="login-container">
         <div class="login-container">
             <div class="change">
             <div class="change">
               <img @click="toOtherLogin" class="typeImg" :src="`${$cdn}images/newLogin/${route.name === 'QrCode'?'login_account.svg':'login_code.svg'}`" alt="">
               <img @click="toOtherLogin" class="typeImg" :src="`${$cdn}images/newLogin/${route.name === 'QrCode'?'login_account.svg':'login_code.svg'}`" alt="">
             </div>
             </div>
             <div class="login-title">
             <div class="login-title">
-              深智云平台
+              {{ t('accountPage.platformTitle') }}
             </div>
             </div>
             <router-view />
             <router-view />
         </div>
         </div>

+ 17 - 17
src/views/account/login.vue

@@ -1,5 +1,5 @@
 <script setup>
 <script setup>
-import { ref, reactive, onBeforeUnmount } from "vue";
+import { ref, reactive, onBeforeUnmount, computed } from "vue";
 import { useRouter } from "vue-router";
 import { useRouter } from "vue-router";
 import { useI18n } from "vue-i18n";
 import { useI18n } from "vue-i18n";
 import { CaretDownFilled, CaretUpFilled } from "@ant-design/icons-vue";
 import { CaretDownFilled, CaretUpFilled } from "@ant-design/icons-vue";
@@ -23,7 +23,7 @@ const formState = reactive({
   password: "",
   password: "",
   msgAuthCode: "",
   msgAuthCode: "",
   remember: false,
   remember: false,
-  selectArea: ["中国", "+86"],
+  selectArea: [t("common.china"), "+86"],
 });
 });
 
 
 const getAreaNum = () => {
 const getAreaNum = () => {
@@ -31,12 +31,12 @@ const getAreaNum = () => {
   return String(code).replace("+", "") || "86";
   return String(code).replace("+", "") || "86";
 };
 };
 
 
-const formRules = {
-  username: [{ required: true, message: "请输入手机号码", trigger: "blur" }],
-  password: [{ required: true, message: "请输入密码", trigger: "blur" }],
+const formRules = computed(() => ({
+  username: [{ required: true, message: t("login.phonePlaceholder"), trigger: "blur" }],
+  password: [{ required: true, message: t("login.passwordPlaceholder"), trigger: "blur" }],
   msgAuthCode: [
   msgAuthCode: [
-    { required: true, message: "请输入验证码", trigger: "blur" },
-    { pattern: /^\d+$/, message: "验证码只能输入数字", trigger: "blur" },
+    { required: true, message: t("login.codePlaceholder"), trigger: "blur" },
+    { pattern: /^\d+$/, message: t("login.codeNumberOnly"), trigger: "blur" },
     {
     {
       validator: async (_rule, value) => {
       validator: async (_rule, value) => {
         if (!value) return Promise.resolve();
         if (!value) return Promise.resolve();
@@ -49,7 +49,7 @@ const formRules = {
       trigger: "blur",
       trigger: "blur",
     },
     },
   ],
   ],
-};
+}));
 
 
 const validateForm = async () => {
 const validateForm = async () => {
   const fields = type.value === "code" ? ["username", "msgAuthCode"] : ["username", "password"];
   const fields = type.value === "code" ? ["username", "msgAuthCode"] : ["username", "password"];
@@ -80,7 +80,7 @@ const changeArea = (item) => {
 const sendCode = async () => {
 const sendCode = async () => {
   if (sendCodeExTime.value > 0) return;
   if (sendCodeExTime.value > 0) return;
   if (!formState.username) {
   if (!formState.username) {
-    message.warning("请输入手机号码");
+    message.warning(t("login.phonePlaceholder"));
     return;
     return;
   }
   }
 
 
@@ -123,10 +123,10 @@ const onSubmit = async () => {
       if (data) {
       if (data) {
         userStore.setToken(data.token);
         userStore.setToken(data.token);
         await userStore.getInfo();
         await userStore.getInfo();
-        message.success("登录成功");
+        message.success(t("login.loginSuccess"));
         router.push({ name: "Home" });
         router.push({ name: "Home" });
       } else {
       } else {
-        message.error("登录失败,请重试!");
+        message.error(t("login.loginFailRetry"));
       }
       }
     } else {
     } else {
       await userStore.login(formState);
       await userStore.login(formState);
@@ -152,14 +152,14 @@ const switchLoginType = (loginType) => {
         :class="{ 'is-active': type === 'user' }"
         :class="{ 'is-active': type === 'user' }"
         @click="switchLoginType('user')"
         @click="switchLoginType('user')"
       >
       >
-        账号密码
+        {{ t("login.accountPassword") }}
       </div>
       </div>
       <div
       <div
         class="login-type-item"
         class="login-type-item"
         :class="{ 'is-active': type === 'code' }"
         :class="{ 'is-active': type === 'code' }"
         @click="switchLoginType('code')"
         @click="switchLoginType('code')"
       >
       >
-        验证码
+        {{ t("login.codeTab") }}
       </div>
       </div>
     </div>
     </div>
     <a-form
     <a-form
@@ -178,7 +178,7 @@ const switchLoginType = (loginType) => {
           v-model:value="formState.username"
           v-model:value="formState.username"
           class="w-full"
           class="w-full"
           size="large"
           size="large"
-          placeholder="请输入手机号码"
+          :placeholder="t('login.phonePlaceholder')"
           :autocomplete="type === 'code' ? 'tel' : 'username'"
           :autocomplete="type === 'code' ? 'tel' : 'username'"
         >
         >
           <template #prefix v-if="type == 'code'">
           <template #prefix v-if="type == 'code'">
@@ -205,7 +205,7 @@ const switchLoginType = (loginType) => {
         <a-input-password
         <a-input-password
           v-model:value="formState.password"
           v-model:value="formState.password"
           size="large"
           size="large"
-          placeholder="请输入密码"
+          :placeholder="t('login.passwordPlaceholder')"
           autocomplete="current-password"
           autocomplete="current-password"
         />
         />
       </a-form-item>
       </a-form-item>
@@ -255,8 +255,8 @@ const switchLoginType = (loginType) => {
         }}</a-button>
         }}</a-button>
       </a-form-item>
       </a-form-item>
       <div class="registerTip text-center text-xs">
       <div class="registerTip text-center text-xs">
-        还没有账号?<span class="cursor-pointer" @click="$router.push('/login/register')"
-          >立即注册</span
+        {{ t("loginAside.loginsub1") }}<span class="cursor-pointer" @click="$router.push('/login/register')"
+          >{{ t("loginAside.loginsub2") }}</span
         >
         >
       </div>
       </div>
     </a-form>
     </a-form>

+ 4 - 2
src/views/account/qrcode.vue

@@ -1,10 +1,12 @@
 <script setup>
 <script setup>
-import { ref, reactive, h, onMounted, onUnmounted} from "vue";
+import { ref, reactive, onUnmounted } from "vue";
+import { useI18n } from "vue-i18n";
 import { useUserStore } from "@/stores";
 import { useUserStore } from "@/stores";
 import QrcodeVue from "qrcode.vue";
 import QrcodeVue from "qrcode.vue";
 import { createLoginQrCode, sendUserInfo } from "@/api/login/index.js";
 import { createLoginQrCode, sendUserInfo } from "@/api/login/index.js";
 import { useRouter, useRoute } from "vue-router";
 import { useRouter, useRoute } from "vue-router";
 import vaiot from "@/assets/image/logo/log.png";
 import vaiot from "@/assets/image/logo/log.png";
+const { t } = useI18n();
 const userStore = useUserStore();
 const userStore = useUserStore();
 const router = useRouter();
 const router = useRouter();
 const imageSettings = reactive({
 const imageSettings = reactive({
@@ -55,7 +57,7 @@ onUnmounted(() => {
 <template>
 <template>
   <div class="login-from">
   <div class="login-from">
     <div class="login-type flex">
     <div class="login-type flex">
-      <div class="login-type-item is-active">扫码登录</div>
+      <div class="login-type-item is-active">{{ t('accountPage.scanLogin') }}</div>
     </div>
     </div>
     <div class="camera-qr qrdiv text-center" @click="getQrCode">
     <div class="camera-qr qrdiv text-center" @click="getQrCode">
       <!-- 带Logo二维码 -->
       <!-- 带Logo二维码 -->

+ 14 - 14
src/views/account/register.vue

@@ -1,5 +1,5 @@
 <script setup>
 <script setup>
-import { ref, reactive, onBeforeUnmount } from "vue";
+import { ref, reactive, onBeforeUnmount, computed } from "vue";
 import { useI18n } from "vue-i18n";
 import { useI18n } from "vue-i18n";
 import { CaretDownFilled, CaretUpFilled } from "@ant-design/icons-vue";
 import { CaretDownFilled, CaretUpFilled } from "@ant-design/icons-vue";
 import phoneAddressSelect from "./components/phoneAddressSelect.vue";
 import phoneAddressSelect from "./components/phoneAddressSelect.vue";
@@ -23,7 +23,7 @@ const formState = reactive({
   confirmPass: "",
   confirmPass: "",
   msgAuthCode: "",
   msgAuthCode: "",
   remember: false,
   remember: false,
-  selectArea: ["中国", "+86"],
+  selectArea: [t("common.china"), "+86"],
 });
 });
 
 
 const getAreaNum = () => {
 const getAreaNum = () => {
@@ -31,26 +31,26 @@ const getAreaNum = () => {
   return String(code).replace("+", "") || "86";
   return String(code).replace("+", "") || "86";
 };
 };
 
 
-const formRules = {
-  username: [{ required: true, message: "请输入手机号码", trigger: "blur" }],
+const formRules = computed(() => ({
+  username: [{ required: true, message: t("login.phonePlaceholder"), trigger: "blur" }],
   msgAuthCode: [
   msgAuthCode: [
-    { required: true, message: "请输入验证码", trigger: "blur" },
-    { pattern: /^\d+$/, message: "验证码只能输入数字", trigger: "blur" },
+    { required: true, message: t("login.codePlaceholder"), trigger: "blur" },
+    { pattern: /^\d+$/, message: t("login.codeNumberOnly"), trigger: "blur" },
   ],
   ],
-  password: [{ required: true, message: "请输入密码", trigger: "blur" }],
+  password: [{ required: true, message: t("login.passwordPlaceholder"), trigger: "blur" }],
   confirmPass: [
   confirmPass: [
-    { required: true, message: "请输入确认密码", trigger: "blur" },
+    { required: true, message: t("login.confirmPassRequired"), trigger: "blur" },
     {
     {
       validator: (_rule, value) => {
       validator: (_rule, value) => {
         if (value && value !== formState.password) {
         if (value && value !== formState.password) {
-          return Promise.reject("两次输入密码不一致");
+          return Promise.reject(t("login.passwordMismatch"));
         }
         }
         return Promise.resolve();
         return Promise.resolve();
       },
       },
       trigger: "blur",
       trigger: "blur",
     },
     },
   ],
   ],
-};
+}));
 
 
 const validateForm = async () => {
 const validateForm = async () => {
   try {
   try {
@@ -81,7 +81,7 @@ const changeArea = (item) => {
 const sendCode = async () => {
 const sendCode = async () => {
   if (sendCodeExTime.value > 0) return;
   if (sendCodeExTime.value > 0) return;
   if (!formState.username) {
   if (!formState.username) {
-    message.warning("请输入手机号码");
+    message.warning(t("login.phonePlaceholder"));
     return;
     return;
   }
   }
 
 
@@ -116,7 +116,7 @@ const onSubmit = async () => {
   try {
   try {
     await validateForm();
     await validateForm();
     if (!formState.remember) {
     if (!formState.remember) {
-      message.warning("请先阅读并同意用户协议");
+      message.warning(t("login.agreeXieyi"));
       return;
       return;
     }
     }
     await userStore.register({
     await userStore.register({
@@ -144,7 +144,7 @@ const handleProtocol = () => {
 <template>
 <template>
   <div class="login-from">
   <div class="login-from">
     <div class="flex login-type">
     <div class="flex login-type">
-      <div class="login-type-item is-active">注册账号</div>
+      <div class="login-type-item is-active">{{ t("login.registerAccount") }}</div>
     </div>
     </div>
     <a-form
     <a-form
       :model="formState"
       :model="formState"
@@ -162,7 +162,7 @@ const handleProtocol = () => {
           v-model:value="formState.username"
           v-model:value="formState.username"
           class="w-full"
           class="w-full"
           size="large"
           size="large"
-          placeholder="请输入手机号码"
+          :placeholder="t('login.phonePlaceholder')"
           autocomplete="tel"
           autocomplete="tel"
         >
         >
           <template #prefix>
           <template #prefix>

+ 28 - 55
src/views/device/components/bindCameraModal.vue

@@ -1,87 +1,74 @@
 <template>
 <template>
   <a-modal
   <a-modal
     v-model:open="visible"
     v-model:open="visible"
-    title="绑定相机"
+    :title="t('deviceAdmin.addDevice')"
     width="620px"
     width="620px"
-    ok-text="确定"
+    :ok-text="t('common.comfirm')"
     :footer="null"
     :footer="null"
-    cancel-text="取消"
+    :cancel-text="t('common.cancel')"
     @ok="handleSubmit"
     @ok="handleSubmit"
     @cancel="handleCancel"
     @cancel="handleCancel"
     :confirm-loading="loading"
     :confirm-loading="loading"
   >
   >
     <a-form ref="formRef" :model="formData" layout="vertical">
     <a-form ref="formRef" :model="formData" layout="vertical">
-      
         <a-form-item
         <a-form-item
           name="sn"
           name="sn"
-          :rules="[
-            {
-              required: true,
-              message: '请输入设备机身的S/N码',
-              trigger: 'blur',
-            },
-          ]"
+          :rules="snRules"
         >
         >
           <a-input
           <a-input
             size="large"
             size="large"
             v-model:value="formData.sn"
             v-model:value="formData.sn"
-            placeholder="请输入设备机身的S/N码"
+            :placeholder="t('deviceAdmin.deviceSnPlaceholder')"
           />
           />
         </a-form-item>
         </a-form-item>
       <div
       <div
         v-for="(item, index) in formData.snList"
         v-for="(item, index) in formData.snList"
         :key="index"
         :key="index"
-        class="sn-item-wrap flex justify-between w-full items-center"
+        class="flex items-center justify-between w-full sn-item-wrap"
       >
       >
         <a-form-item
         <a-form-item
-        class="w-full flex-1"
+        class="flex-1 w-full"
           :name="['snList', index, 'sn']"
           :name="['snList', index, 'sn']"
-          :rules="[
-            {
-              required: true,
-              message: '请输入设备机身的S/N码',
-              trigger: 'blur',
-            },
-          ]"
+          :rules="snRules"
         >
         >
           <a-input
           <a-input
             size="large"
             size="large"
             v-model:value="item.sn"
             v-model:value="item.sn"
-            placeholder="请输入设备机身的S/N码"
+            :placeholder="t('deviceAdmin.deviceSnPlaceholder')"
           />
           />
         </a-form-item>
         </a-form-item>
-        <!-- 仅第一条不展示删除按钮,其余展示 -->
         <div class="del-btn" @click="removeItem(index)">
         <div class="del-btn" @click="removeItem(index)">
         <MinusCircleFilled  style="color: red; font-size: 16px" />
         <MinusCircleFilled  style="color: red; font-size: 16px" />
         </div>
         </div>
       </div>
       </div>
     </a-form>
     </a-form>
-    <!-- 底部:复选框 + 按钮 -->
-    <div class="ant-modal-footer modal-footer flex justify-between">
+    <div class="flex justify-between ant-modal-footer modal-footer">
       <a-button type="primary" ghost @click="addItem">
       <a-button type="primary" ghost @click="addItem">
-        +再添加一台相机
+        +{{ t('mall.myOrders.bindCameraModule.reNewDevice') }}
       </a-button>
       </a-button>
       <div class="btn-group">
       <div class="btn-group">
-        <a-button style="margin-right: 16px" @click="handleCancel">取消</a-button>
-        <a-button type="primary" @click="handleSubmit">确定</a-button>
+        <a-button style="margin-right: 16px" @click="handleCancel">{{ t('common.cancel') }}</a-button>
+        <a-button type="primary" @click="handleSubmit">{{ t('common.comfirm') }}</a-button>
       </div>
       </div>
     </div>
     </div>
   </a-modal>
   </a-modal>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, reactive, watch, defineProps, defineEmits } from "vue";
+import { ref, reactive, watch, computed } from "vue";
+import { useI18n } from "vue-i18n";
 import {
 import {
   MinusCircleFilled,
   MinusCircleFilled,
 } from "@ant-design/icons-vue";
 } from "@ant-design/icons-vue";
 import { message } from "ant-design-vue";
 import { message } from "ant-design-vue";
 import { cameraAdd } from "@/api/equity";
 import { cameraAdd } from "@/api/equity";
-// 父组件控制弹窗显隐
+
+const { t } = useI18n();
+
 const props = defineProps({
 const props = defineProps({
   open: {
   open: {
     type: Boolean,
     type: Boolean,
     default: false,
     default: false,
-    description: "弹窗是否可见",
   },
   },
   cameraType: {
   cameraType: {
     type: Number,
     type: Number,
@@ -93,33 +80,32 @@ const emit = defineEmits(["update:open", "confirm"]);
 const visible = ref(props.open);
 const visible = ref(props.open);
 const loading = ref(false);
 const loading = ref(false);
 const formRef = ref();
 const formRef = ref();
-const sn = ref("");
-// 初始化表单:默认1条输入框
+
 const formData = reactive({
 const formData = reactive({
   snList: [],
   snList: [],
   sn: "",
   sn: "",
 });
 });
 
 
-// 新增一条SN输入
+const snRules = computed(() => [{
+  required: true,
+  message: t('deviceAdmin.deviceSnPlaceholder'),
+  trigger: 'blur',
+}]);
+
 const addItem = () => {
 const addItem = () => {
   formData.snList.push({ sn: "" });
   formData.snList.push({ sn: "" });
 };
 };
 
 
-// 删除指定行,最少保留1条
 const removeItem = (index) => {
 const removeItem = (index) => {
   if (formData.snList.length <= 0) return;
   if (formData.snList.length <= 0) return;
   formData.snList.splice(index, 1);
   formData.snList.splice(index, 1);
-  // 清除对应行校验提示
   formRef.value?.clearValidate(["snList", index, "sn"]);
   formRef.value?.clearValidate(["snList", index, "sn"]);
 };
 };
 
 
-// 弹窗打开/关闭同步
 watch(
 watch(
   () => props.open,
   () => props.open,
   (val) => {
   (val) => {
-  console.log("visible", val);
     visible.value = val;
     visible.value = val;
-    // 关闭弹窗重置表单
     if (!val) {
     if (!val) {
       formData.snList = [];
       formData.snList = [];
       formRef.value?.resetFields();
       formRef.value?.resetFields();
@@ -128,28 +114,24 @@ watch(
   { immediate: true }
   { immediate: true }
 );
 );
 
 
-// 确定提交
 const handleSubmit = async () => {
 const handleSubmit = async () => {
   try {
   try {
     loading.value = true;
     loading.value = true;
     const sndata = await formRef.value?.validate();
     const sndata = await formRef.value?.validate();
-    console.log("formRef", sndata);
     let list = formData.snList?.map((item) => item.sn) || [];
     let list = formData.snList?.map((item) => item.sn) || [];
     list.push(sndata.sn)
     list.push(sndata.sn)
-    // 抛出所有SN数组给父组件
     const snArr = formData.snList.map((item) => item.sn.trim());
     const snArr = formData.snList.map((item) => item.sn.trim());
-    await cameraAdd({snCodes: list.join(',') })
+    await cameraAdd({snCode: list.join(',') })
     message.success(t('common.success'));
     message.success(t('common.success'));
     emit("confirm", snArr);
     emit("confirm", snArr);
     emit("update:open", false);
     emit("update:open", false);
   } catch (err) {
   } catch (err) {
-    console.log("校验失败", err);
+    console.log("validate failed", err);
   } finally {
   } finally {
     loading.value = false;
     loading.value = false;
   }
   }
 };
 };
 
 
-// 取消关闭
 const handleCancel = () => {
 const handleCancel = () => {
   emit("update:open", false);
   emit("update:open", false);
 };
 };
@@ -164,7 +146,6 @@ const handleCancel = () => {
   flex-shrink: 0;
   flex-shrink: 0;
   width: 26px;
   width: 26px;
   height: 26px;
   height: 26px;
-  // background: #f53f3f;
   border-radius: 50%;
   border-radius: 50%;
   display: flex;
   display: flex;
   margin-bottom: 24px;
   margin-bottom: 24px;
@@ -172,13 +153,5 @@ const handleCancel = () => {
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
   cursor: pointer;
   cursor: pointer;
-  .minus-icon {
-    color: #fff;
-    font-size: 16px;
-    font-weight: bold;
-  }
-}
-.add-btn-wrap {
-  margin-top: 8px;
 }
 }
-</style>
+</style>

+ 1 - 1
src/views/device/components/cooperationModal.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
   <a-modal 
   <a-modal 
-    title="协作设置" 
+    :title="t('deviceAdmin.cooperationTitle')" 
     v-model:open="open"
     v-model:open="open"
     :width="500"
     :width="500"
     @cancel="handleCancels" 
     @cancel="handleCancels" 

+ 5 - 13
src/views/device/index.vue

@@ -22,9 +22,6 @@ import cooperationModal from "./components/cooperationModal.vue";
 import {
 import {
   equityColumns,
   equityColumns,
   downColumns,
   downColumns,
-  getStatusImg,
-  getStatusText,
-  SceneTypePaths,
   getCameraType,
   getCameraType,
 } from "./data";
 } from "./data";
 const { t } = useI18n();
 const { t } = useI18n();
@@ -46,7 +43,6 @@ const cooperation = ref({
   ids: [],
   ids: [],
 });
 });
 
 
-const searchType = ref("name");
 const open = ref(false);
 const open = ref(false);
 const bindModalOpen = ref(false);
 const bindModalOpen = ref(false);
 const loading = ref(false);
 const loading = ref(false);
@@ -98,10 +94,6 @@ const searchClick = () => {
   selectTable.value = [];
   selectTable.value = [];
   baseTableRef.value.resetPage();
   baseTableRef.value.resetPage();
 };
 };
-const handleSee = (record) => {
-  let path = SceneTypePaths[record.sceneSource];
-  window.open(path[0], "_blank");
-};
 const handleCooperation = (record) => {
 const handleCooperation = (record) => {
   cooperation.value = {
   cooperation.value = {
     show: true,
     show: true,
@@ -148,7 +140,7 @@ watch([() => cameraType.value, ()=>childName.value], () => {
 </script>
 </script>
 <template>
 <template>
   <div class="scene-layout">
   <div class="scene-layout">
-    <div class="routeName">设备管理</div>
+    <div class="routeName">{{ t('menu.deviceManage') }}</div>
     <div class="table w-full">
     <div class="table w-full">
       <BaseTable
       <BaseTable
         :columns="activeKey == '0' ? equityColumns : downColumns"
         :columns="activeKey == '0' ? equityColumns : downColumns"
@@ -166,11 +158,11 @@ watch([() => cameraType.value, ()=>childName.value], () => {
         <!-- 顶部搜索插槽 -->
         <!-- 顶部搜索插槽 -->
         <template #header>
         <template #header>
           <div
           <div
-            class="tableHeader flex justify-between items-center mb-5 w-full"
+            class="flex items-center justify-between w-full mb-5 tableHeader"
             v-if="ids.length == 0"
             v-if="ids.length == 0"
           >
           >
             <div
             <div
-              class="headerLeft flex justify-start items-center gap-x-4 position-relative"
+              class="flex items-center justify-start headerLeft gap-x-4 position-relative"
             >
             >
               <a-space >
               <a-space >
                 <a-select
                 <a-select
@@ -190,7 +182,7 @@ watch([() => cameraType.value, ()=>childName.value], () => {
                   style="width: 400px"
                   style="width: 400px"
                   size="large"
                   size="large"
                   allowClear
                   allowClear
-                  placeholder="请输入S/N码搜索"
+                  :placeholder="t('deviceAdmin.snSearchPlaceholder')"
                 >
                 >
                   <template #prefix><SearchOutlined /></template>
                   <template #prefix><SearchOutlined /></template>
                 </a-input>
                 </a-input>
@@ -200,7 +192,7 @@ watch([() => cameraType.value, ()=>childName.value], () => {
               </a-button>
               </a-button>
           </div>
           </div>
           <div
           <div
-            class="tableHeader flex justify-end items-center mb-5 w-full"
+            class="flex items-center justify-end w-full mb-5 tableHeader"
             v-else
             v-else
           >
           >
             <a-space size="middle">
             <a-space size="middle">

+ 6 - 2
src/views/equity/components/bindCameraModal.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
   <a-modal 
   <a-modal 
-    title="授权相机" 
+    :title="t('deviceAdmin.authCameraTitle')" 
     :open="visible" 
     :open="visible" 
     @cancel="handleCancel" 
     @cancel="handleCancel" 
     :width="500"
     :width="500"
@@ -29,9 +29,13 @@
 
 
 <script setup>
 <script setup>
 import { ref, reactive, defineEmits, defineProps, watch, onUnmounted } from "vue";
 import { ref, reactive, defineEmits, defineProps, watch, onUnmounted } from "vue";
+import { useI18n } from "vue-i18n";
 import { debounce } from "lodash-es";
 import { debounce } from "lodash-es";
+import { message } from "ant-design-vue";
 
 
 // 1. 定义Props(增加类型校验和注释)
 // 1. 定义Props(增加类型校验和注释)
+const { t } = useI18n();
+
 const props = defineProps({
 const props = defineProps({
   visible: {
   visible: {
     type: Boolean,
     type: Boolean,
@@ -148,7 +152,7 @@ const handleConfirm = () => {
   }
   }
 
 
   // 提示用户(可替换为UI组件库的message)
   // 提示用户(可替换为UI组件库的message)
-  alert("请输入重命名内容或选择用户!");
+  message.warning(t('mall.myOrders.bindCameraModule.inputSnCode'));
 };
 };
 </script>
 </script>
 
 

+ 6 - 6
src/views/equity/index.vue

@@ -1,5 +1,5 @@
 <script setup>
 <script setup>
-import { ref, h, onMounted, createVNode, watch } from "vue";
+import { ref, h, onMounted, createVNode, watch, computed } from "vue";
 import { useRouter, useRoute } from "vue-router";
 import { useRouter, useRoute } from "vue-router";
 import { useI18n } from "vue-i18n";
 import { useI18n } from "vue-i18n";
 import { Modal, message } from 'ant-design-vue';
 import { Modal, message } from 'ant-design-vue';
@@ -32,14 +32,14 @@ console.log(query, "route");
 const activeKey = ref(query.type || "0");
 const activeKey = ref(query.type || "0");
 const useType = ref(null);
 const useType = ref(null);
 
 
-const tablist = ref([
+const tablist = computed(() => [
   {
   {
-    label: '会员权益',
+    label: t('mall.member.privilege'),
     value: "0",
     value: "0",
     key: "0",
     key: "0",
   },
   },
   {
   {
-    label: '场景下载',
+    label: t('mall.member.sceneDownload'),
     value: "1",
     value: "1",
     key: "1",
     key: "1",
   },
   },
@@ -122,7 +122,7 @@ const hendleCooperation = () => {
         title: t('common.tip'),
         title: t('common.tip'),
         icon: createVNode(ExclamationCircleFilled),
         icon: createVNode(ExclamationCircleFilled),
         centered: true,
         centered: true,
-        content: '计算中/已封存的场景无法协作',
+        content: t('scene.sceneAdmin.cooperationBlockedTip'),
         // okText: '确认',
         // okText: '确认',
         // cancelText: '取消',
         // cancelText: '取消',
         onOk: async () => {
         onOk: async () => {
@@ -151,7 +151,7 @@ const handleEquity = (record)=> {
         title: t('common.tip'),
         title: t('common.tip'),
         icon: createVNode(ExclamationCircleFilled),
         icon: createVNode(ExclamationCircleFilled),
         centered: true,
         centered: true,
-        content: '确定要解除该相机的权益吗?',
+        content: t('member.unbindEquityTip'),
         // okText: '确认',
         // okText: '确认',
         // cancelText: '取消',
         // cancelText: '取消',
         onOk: async () => {
         onOk: async () => {

+ 8 - 5
src/views/home/components/notice.vue

@@ -1,12 +1,15 @@
 <script setup>
 <script setup>
 import { RightOutlined } from "@ant-design/icons-vue";
 import { RightOutlined } from "@ant-design/icons-vue";
-import { ref } from "vue";
+import { computed } from "vue";
+import { useI18n } from "vue-i18n";
 
 
-const notice = ref({
-  title: "功能更新",
-  content: "点位校准功能更新了支持全站仪控制点校准,来试试吧!",
+const { t } = useI18n();
+
+const notice = computed(() => ({
+  title: t("homePage.noticeTitle"),
+  content: t("homePage.noticeContent"),
   path: "/home/scene/pointcloud",
   path: "/home/scene/pointcloud",
-});
+}));
 </script>
 </script>
 
 
 <template>
 <template>

+ 39 - 29
src/views/home/index.vue

@@ -1,5 +1,6 @@
 <script setup>
 <script setup>
-import { ref, h, onMounted } from 'vue'
+import { ref, computed, onMounted } from 'vue'
+import { useI18n } from 'vue-i18n'
 import homeNotice from './components/notice.vue'
 import homeNotice from './components/notice.vue'
 import LeafletMap from './components/map.vue'
 import LeafletMap from './components/map.vue'
 import myModal from '@/components/myModal.vue'
 import myModal from '@/components/myModal.vue'
@@ -21,6 +22,7 @@ import { useUserStore } from '@/stores'
 import { userSerciceList } from '@/api/mall'
 import { userSerciceList } from '@/api/mall'
 import { hasPanoramaUserService, PANORAMA_SERVICE_ID } from '@/views/mall/data.ts'
 import { hasPanoramaUserService, PANORAMA_SERVICE_ID } from '@/views/mall/data.ts'
 
 
+const { t } = useI18n()
 const open = ref(false)
 const open = ref(false)
 const router = useRouter()
 const router = useRouter()
 const user = useUserStore()
 const user = useUserStore()
@@ -28,40 +30,48 @@ const mapRef = ref(null)
 const [modal, contextHolder] = Modal.useModal();
 const [modal, contextHolder] = Modal.useModal();
 const { openModal } = useModal()
 const { openModal } = useModal()
 
 
-const allEquityList = [
+const allEquityListBase = [
   {
   {
     id: 2,
     id: 2,
-    title: '离线包下载',
+    titleKey: 'mall.featureDownload',
     img: down,
     img: down,
     path: '/mall/sercice/2'
     path: '/mall/sercice/2'
   },{
   },{
     id: PANORAMA_SERVICE_ID,
     id: PANORAMA_SERVICE_ID,
-    title: '四维全景',
+    titleKey: 'homePage.panorama',
     img: qjkk,
     img: qjkk,
     path: '/mall/sercice/1'
     path: '/mall/sercice/1'
   },{
   },{
-    title: 'VR小程序对接',
+    titleKey: 'homePage.vrMiniProgram',
     img: vr,
     img: vr,
     url: 'https://affim.baidu.com/unique_54577246/chat?siteId=22911125&userId=54577246&siteToken=29903ef13e9612af77266f2b97d1c715 '
     url: 'https://affim.baidu.com/unique_54577246/chat?siteId=22911125&userId=54577246&siteToken=29903ef13e9612af77266f2b97d1c715 '
   },{
   },{
     id: 3,
     id: 3,
-    title: 'AI训练',
+    titleKey: 'homePage.aiTraining',
     img: ai,
     img: ai,
     path: '/mall/sercice/3'
     path: '/mall/sercice/3'
   },
   },
 ]
 ]
 
 
-const equityList = ref([...allEquityList])
+const allEquityList = computed(() =>
+  allEquityListBase.map((item) => ({
+    ...item,
+    title: t(item.titleKey),
+  }))
+)
+
+const equityList = ref([])
 
 
 const loadEquityList = async () => {
 const loadEquityList = async () => {
+  const list = allEquityList.value
   try {
   try {
     const userList = await userSerciceList()
     const userList = await userSerciceList()
     const showPanorama = hasPanoramaUserService(userList)
     const showPanorama = hasPanoramaUserService(userList)
-    equityList.value = allEquityList.filter(
+    equityList.value = list.filter(
       (item) => item.id !== PANORAMA_SERVICE_ID || showPanorama
       (item) => item.id !== PANORAMA_SERVICE_ID || showPanorama
     )
     )
   } catch {
   } catch {
-    equityList.value = allEquityList.filter((item) => item.id !== PANORAMA_SERVICE_ID)
+    equityList.value = list.filter((item) => item.id !== PANORAMA_SERVICE_ID)
   }
   }
 }
 }
 
 
@@ -79,33 +89,33 @@ const handleServes = async (item) => {
 
 
 </script>
 </script>
 <template>
 <template>
-  <div class="layout-home">
+  <div class="layout-home" style="padding-bottom: 60px">
     <homeNotice />
     <homeNotice />
-    <div class="equityServer flex gap-10">
-      <div class="equity flex-1">
-        <div class="title">我的权益</div>
-        <div class="list flex gap-4">
-          <div class="member-item flex-1 cursor-pointer" @click="$router.push('/equity')">
+    <div class="flex gap-10 equityServer">
+      <div class="flex-1 equity">
+        <div class="title">{{ $t('homePage.myEquity') }}</div>
+        <div class="flex gap-4 list">
+          <div class="flex-1 cursor-pointer member-item" @click="$router.push('/equity')">
             <!-- 左侧图标 -->
             <!-- 左侧图标 -->
             <div class="icon-box">
             <div class="icon-box">
-              <img src="@/assets/image/home/equity.png" alt="会员">
+              <img src="@/assets/image/home/equity.png" :alt="$t('menu.memberEquity')">
             </div>
             </div>
             <!-- 中间文字,左对齐 -->
             <!-- 中间文字,左对齐 -->
             <div class="text-group" >
             <div class="text-group" >
-              <div class="name">会员权益</div>
+              <div class="name">{{ $t('menu.memberEquity') }}</div>
               <div class="num">{{user.incrementNum}}/{{user.incrementBindNum}}</div>
               <div class="num">{{user.incrementNum}}/{{user.incrementBindNum}}</div>
             </div>
             </div>
             <!-- 右侧箭头 -->
             <!-- 右侧箭头 -->
             <RightOutlined style="font-size: 12px" />
             <RightOutlined style="font-size: 12px" />
           </div>
           </div>
-          <div class="member-item flex-1 cursor-pointer" @click="$router.push('/equity?type=1')">
+          <div class="flex-1 cursor-pointer member-item" @click="$router.push('/equity?type=1')">
             <!-- 左侧图标 -->
             <!-- 左侧图标 -->
             <div class="icon-box">
             <div class="icon-box">
-              <img src="@/assets/image/home/downloads.png" alt="会员">
+              <img src="@/assets/image/home/downloads.png" :alt="$t('menu.memberEquity')">
             </div>
             </div>
             <!-- 中间文字,左对齐 -->
             <!-- 中间文字,左对齐 -->
             <div class="text-group">
             <div class="text-group">
-              <div class="name">剩余下载</div>
+              <div class="name">{{ $t('information.expiredDownload') }}</div>
               <div class="num">{{user.downloadNumTotal - user.downloadNum}}</div>
               <div class="num">{{user.downloadNumTotal - user.downloadNum}}</div>
             </div>
             </div>
             <!-- 右侧箭头 -->
             <!-- 右侧箭头 -->
@@ -114,15 +124,15 @@ const handleServes = async (item) => {
         </div>
         </div>
       </div>
       </div>
       <div class="equity flex-2">
       <div class="equity flex-2">
-        <div class="title flex justify-between items-center">
-          <span>增值服务</span>
-          <span class="text-sm text-blue-500 cursor-pointer" @click="$router.push('/mall')">查看更多</span>
+        <div class="flex items-center justify-between title">
+          <span>{{ $t('mall.valueAddedTitle') }}</span>
+          <span class="text-sm text-blue-500 cursor-pointer" @click="$router.push('/mall')">{{ $t('homePage.viewMore') }}</span>
         </div>
         </div>
-        <div class="list flex gap-4">
-          <div class="member-item flex-1 cursor-pointer" @click="handleServes(item)" v-for="item in equityList" :key="item.title">
+        <div class="flex gap-4 list">
+          <div class="flex-1 cursor-pointer member-item" @click="handleServes(item)" v-for="item in equityList" :key="item.title">
             <!-- 左侧图标 -->
             <!-- 左侧图标 -->
             <div class="icon-box equitybox">
             <div class="icon-box equitybox">
-              <img :src="item.img" alt="会员">
+              <img :src="item.img" :alt="$t('menu.memberEquity')">
             </div>
             </div>
             <!-- 中间文字,左对齐 -->
             <!-- 中间文字,左对齐 -->
             <div class="text-group">
             <div class="text-group">
@@ -136,8 +146,8 @@ const handleServes = async (item) => {
       </div>
       </div>
     </div>
     </div>
     <div class="map">
     <div class="map">
-      <div class="title flex justify-between">
-        <span>地区分布</span>
+      <div class="flex justify-between title">
+        <span>{{ $t('homePage.regionDistribution') }}</span>
         <FullscreenOutlined class="cursor-pointer" @click="handleMap" />
         <FullscreenOutlined class="cursor-pointer" @click="handleMap" />
       </div>
       </div>
       <div class="map-content">
       <div class="map-content">
@@ -218,4 +228,4 @@ const handleServes = async (item) => {
     height: 522px;
     height: 522px;
   }
   }
 }
 }
-</style>
+</style>

+ 13 - 10
src/views/information/components/brandInfo.vue

@@ -1,19 +1,19 @@
 <template>
 <template>
   <div class="scene-logo-setting" style="max-width: 720px;">
   <div class="scene-logo-setting" style="max-width: 720px;">
     <div class="form-item">
     <div class="form-item">
-      <div class="label">场景简介</div>
+      <div class="label">{{ $t('information.brandSetting.sceneIntro') }}</div>
       <a-input
       <a-input
         size="large"
         size="large"
         style="width: 400px"
         style="width: 400px"
         v-model:value="formData.description"
         v-model:value="formData.description"
-        placeholder="请输入场景简介"
+        :placeholder="$t('information.brandSetting.sceneIntroPlaceholder')"
         @blur="handleDescriptionSave"
         @blur="handleDescriptionSave"
         @pressEnter="handleDescriptionSave"
         @pressEnter="handleDescriptionSave"
       />
       />
     </div>
     </div>
 
 
     <div class="form-item">
     <div class="form-item">
-      <div class="label">二维码Logo</div>
+      <div class="label">{{ $t('information.brandSetting.qrLogo') }}</div>
       <div class="upload-list">
       <div class="upload-list">
         <div
         <div
           class="upload-card"
           class="upload-card"
@@ -31,7 +31,7 @@
     </div>
     </div>
 
 
     <div class="form-item">
     <div class="form-item">
-      <div class="label">地面logo</div>
+      <div class="label">{{ $t('information.brandSetting.floorLogo') }}</div>
       <div class="upload-list">
       <div class="upload-list">
         <div
         <div
           class="upload-card"
           class="upload-card"
@@ -49,7 +49,7 @@
     </div>
     </div>
 
 
     <div class="form-item">
     <div class="form-item">
-      <div class="label">地面logo</div>
+      <div class="label">{{ $t('information.brandSetting.loadingLogo') }}</div>
       <div class="upload-list">
       <div class="upload-list">
         <div
         <div
           class="upload-card"
           class="upload-card"
@@ -80,9 +80,12 @@
 import { ref, reactive } from 'vue'
 import { ref, reactive } from 'vue'
 import { message } from 'ant-design-vue'
 import { message } from 'ant-design-vue'
 import { LoadingOutlined } from '@ant-design/icons-vue'
 import { LoadingOutlined } from '@ant-design/icons-vue'
+import { useI18n } from 'vue-i18n'
 import { userBrandInfo, setuserBrand } from '@/api/equity'
 import { userBrandInfo, setuserBrand } from '@/api/equity'
 import { useOssUpload } from '@/hooks/useOssUpload'
 import { useOssUpload } from '@/hooks/useOssUpload'
 
 
+const { t } = useI18n()
+
 type LogoField = 'shareLogo' | 'floorLogo' | 'loadingLogo'
 type LogoField = 'shareLogo' | 'floorLogo' | 'loadingLogo'
 
 
 interface BrandInfo {
 interface BrandInfo {
@@ -99,7 +102,7 @@ const lastSavedDescription = ref('')
 const isInitialized = ref(false)
 const isInitialized = ref(false)
 
 
 const formData = reactive({
 const formData = reactive({
-  description: '四维看看,让空间讲故事',
+  description: t('header.footer.bannerTitle'),
   shareLogo: '',
   shareLogo: '',
   floorLogo: '',
   floorLogo: '',
   loadingLogo: '',
   loadingLogo: '',
@@ -135,7 +138,7 @@ const handleDescriptionSave = async () => {
   if (!isInitialized.value) return
   if (!isInitialized.value) return
   if (formData.description === lastSavedDescription.value) return
   if (formData.description === lastSavedDescription.value) return
   await saveBrandInfo()
   await saveBrandInfo()
-  message.success('保存成功')
+  message.success(t('information.brandSetting.saveSuccess'))
 }
 }
 
 
 const handleFileSelect = async (event: Event) => {
 const handleFileSelect = async (event: Event) => {
@@ -152,7 +155,7 @@ const handleFileSelect = async (event: Event) => {
     if (result?.ossPath) {
     if (result?.ossPath) {
       formData[field] = result.ossPath
       formData[field] = result.ossPath
       await saveBrandInfo()
       await saveBrandInfo()
-      message.success('上传并保存成功')
+      message.success(t('information.brandSetting.uploadSaveSuccess'))
     }
     }
   } catch {
   } catch {
     // 错误提示由 hook 统一处理
     // 错误提示由 hook 统一处理
@@ -165,7 +168,7 @@ const handleFileSelect = async (event: Event) => {
 const removeLogo = async (field: LogoField) => {
 const removeLogo = async (field: LogoField) => {
   formData[field] = ''
   formData[field] = ''
   await saveBrandInfo()
   await saveBrandInfo()
-  message.success('图片已移除')
+  message.success(t('information.brandSetting.imageRemoved'))
 }
 }
 
 
 const getInfo = async () => {
 const getInfo = async () => {
@@ -185,7 +188,7 @@ const getInfo = async () => {
 
 
 const setInfo = async () => {
 const setInfo = async () => {
   await saveBrandInfo()
   await saveBrandInfo()
-  message.success('保存成功')
+  message.success(t('information.brandSetting.saveSuccess'))
 }
 }
 
 
 getInfo()
 getInfo()

+ 37 - 42
src/views/information/components/info.vue

@@ -2,7 +2,7 @@
   <div class="user-profile-page">
   <div class="user-profile-page">
     <div class="profile-item">
     <div class="profile-item">
       <div class="item-left">
       <div class="item-left">
-        <div class="label">账号</div>
+        <div class="label">{{ $t('information.userName') }}</div>
         <div class="value">{{ user.userName }}</div>
         <div class="value">{{ user.userName }}</div>
       </div>
       </div>
       <div class="item-right"></div>
       <div class="item-right"></div>
@@ -10,27 +10,27 @@
 
 
     <div class="profile-item">
     <div class="profile-item">
       <div class="item-left">
       <div class="item-left">
-        <div class="label">昵称</div>
+        <div class="label">{{ $t('information.nickName') }}</div>
         <div class="value">{{ user.nickName }}</div>
         <div class="value">{{ user.nickName }}</div>
       </div>
       </div>
       <div class="item-right">
       <div class="item-right">
-        <a-button @click="openNickModal">更改昵称</a-button>
+        <a-button @click="openNickModal">{{ $t('information.changeNickName') }}</a-button>
       </div>
       </div>
     </div>
     </div>
 
 
     <div class="profile-item">
     <div class="profile-item">
       <div class="item-left">
       <div class="item-left">
-        <div class="label">密码</div>
+        <div class="label">{{ $t('login.password') }}</div>
         <div class="value">***************</div>
         <div class="value">***************</div>
       </div>
       </div>
       <div class="item-right">
       <div class="item-right">
-        <a-button @click="openPwdModal">更改密码</a-button>
+        <a-button @click="openPwdModal">{{ $t('information.changePasswordBtn') }}</a-button>
       </div>
       </div>
     </div>
     </div>
 
 
     <div class="profile-item">
     <div class="profile-item">
       <div class="item-left">
       <div class="item-left">
-        <div class="label">收货地址</div>
+        <div class="label">{{ $t('mall.receiveAddress') }}</div>
         <div class="value">
         <div class="value">
           {{ receiverInfo.shipAreaPath }}{{ receiverInfo.shipAddress }}-{{
           {{ receiverInfo.shipAreaPath }}{{ receiverInfo.shipAddress }}-{{
             receiverInfo.shipName
             receiverInfo.shipName
@@ -38,32 +38,29 @@
         </div>
         </div>
       </div>
       </div>
       <div class="item-right">
       <div class="item-right">
-        <a-button @click="openAddrModal">修改地址</a-button>
+        <a-button @click="openAddrModal">{{ $t('information.editAddressLinkText') }}</a-button>
       </div>
       </div>
     </div>
     </div>
 
 
     <!-- 1. 更改昵称弹窗 优化版 -->
     <!-- 1. 更改昵称弹窗 优化版 -->
     <a-modal
     <a-modal
       v-model:open="nickVisible"
       v-model:open="nickVisible"
-      title="更改昵称"
+      :title="$t('information.changeNickName')"
       :width="446"
       :width="446"
-      ok-text="确定"
-      cancel-text="取消"
+      :ok-text="$t('common.comfirm')"
+      :cancel-text="$t('common.cancel')"
       @ok="submitNick"
       @ok="submitNick"
     >
     >
       <a-form ref="nickRef" :model="userInfo" layout="vertical">
       <a-form ref="nickRef" :model="userInfo" layout="vertical">
         <a-form-item
         <a-form-item
           name="nickName"
           name="nickName"
-          :rules="[
-            { required: true, message: '请输入昵称' },
-            { max: 50, message: '最多50个字符' },
-          ]"
+          :rules="nickRules"
         >
         >
           <a-input
           <a-input
             size="large"
             size="large"
             allowClear
             allowClear
             v-model:value="userInfo.nickName"
             v-model:value="userInfo.nickName"
-            placeholder="请输入昵称"
+            :placeholder="$t('information.nickNamePlaceholder')"
             show-count
             show-count
             maxlength="50"
             maxlength="50"
           />
           />
@@ -74,10 +71,10 @@
     <!-- 2. 修改密码弹窗 优化版(双区号+验证码倒计时) -->
     <!-- 2. 修改密码弹窗 优化版(双区号+验证码倒计时) -->
     <a-modal
     <a-modal
       v-model:open="pwdVisible"
       v-model:open="pwdVisible"
-      title="修改密码"
+      :title="$t('information.changePassword.changePassword')"
       width="440px"
       width="440px"
-      ok-text="确定"
-      cancel-text="取消"
+      :ok-text="$t('common.comfirm')"
+      :cancel-text="$t('common.cancel')"
       @ok="submitPwd"
       @ok="submitPwd"
     >
     >
       <a-form
       <a-form
@@ -102,7 +99,7 @@
             disabled
             disabled
             size="large"
             size="large"
             v-model:value="user.userName"
             v-model:value="user.userName"
-            placeholder="请输入手机号"
+            :placeholder="$t('login.phonePlaceholder')"
             maxlength="11"
             maxlength="11"
             autocomplete="off"
             autocomplete="off"
             readonly
             readonly
@@ -142,7 +139,7 @@
           <a-input-password
           <a-input-password
             size="large"
             size="large"
             v-model:value="pwdForm.password"
             v-model:value="pwdForm.password"
-            placeholder="设置密码"
+            :placeholder="$t('login.setPassword')"
             autocomplete="new-password"
             autocomplete="new-password"
             name="new-password"
             name="new-password"
           />
           />
@@ -153,7 +150,7 @@
           <a-input-password
           <a-input-password
             size="large"
             size="large"
             v-model:value="pwdForm.confirmPwd"
             v-model:value="pwdForm.confirmPwd"
-            placeholder="确认密码"
+            :placeholder="$t('login.confirmPassword')"
             autocomplete="new-password"
             autocomplete="new-password"
             name="confirm-new-password"
             name="confirm-new-password"
           />
           />
@@ -202,6 +199,10 @@ const addrVisible = ref(false);
 // ========== 昵称弹窗表单 ==========
 // ========== 昵称弹窗表单 ==========
 const nickRef = ref<FormInstance>();
 const nickRef = ref<FormInstance>();
 const nickForm = reactive({ nick: "" });
 const nickForm = reactive({ nick: "" });
+const nickRules = computed(() => [
+  { required: true, message: t("information.nickNamePlaceholder"), trigger: "blur" },
+  { max: 50, message: t("information.nickNameMaxLength"), trigger: "blur" },
+]);
 const openNickModal = () => {
 const openNickModal = () => {
   userInfo.nickName = user.nickName;
   userInfo.nickName = user.nickName;
   nickVisible.value = true;
   nickVisible.value = true;
@@ -211,19 +212,14 @@ const submitNick = async () => {
   await nickRef.value?.validate();
   await nickRef.value?.validate();
   await user.updateInfo(userInfo.nickName);
   await user.updateInfo(userInfo.nickName);
   nickVisible.value = false;
   nickVisible.value = false;
-  message.success("昵称修改成功");
+  message.success(t("information.nickNameChangeSuccess"));
   // 调用后端更新昵称接口
   // 调用后端更新昵称接口
 };
 };
 // 表单校验规则
 // 表单校验规则
-const formRules: Record<string, Rule[]> = {
-  phone: [
-    { required: true, message: "请输入手机号", trigger: "blur" },
-    { pattern: /^1[3-9]\d{9}$/, message: "手机号格式错误", trigger: "blur" },
-  ],
-  // 验证码:本地格式校验 + 异步远程校验
+const formRules = computed(() => ({
   msgAuthCode: [
   msgAuthCode: [
-    { required: true, message: "请输入验证码", trigger: "blur" },
-    { pattern: /^\d+$/, message: "验证码只能输入数字", trigger: "blur" },
+    { required: true, message: t("login.codePlaceholder"), trigger: "blur" },
+    { pattern: /^\d+$/, message: t("login.codeNumberOnly"), trigger: "blur" },
     {
     {
       validator: async (_rule, value) => {
       validator: async (_rule, value) => {
         if (!value) return Promise.resolve();
         if (!value) return Promise.resolve();
@@ -238,27 +234,26 @@ const formRules: Record<string, Rule[]> = {
     },
     },
   ],
   ],
   password: [
   password: [
-    { required: true, message: "请设置密码", trigger: "blur" },
+    { required: true, message: t("login.setPassword"), trigger: "blur" },
     {
     {
       pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z0-9]{8,16}$/,
       pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z0-9]{8,16}$/,
-      message: "密码需8-16位,同时包含大写字母、小写字母、数字",
+      message: t("login.passwordTip"),
       trigger: "blur",
       trigger: "blur",
     },
     },
   ],
   ],
-  // 确认密码 联动密码校验
   confirmPwd: [
   confirmPwd: [
-    { required: true, message: "请再次输入密码", trigger: "blur" },
+    { required: true, message: t("login.confirmPassRequired"), trigger: "blur" },
     {
     {
       validator: (_rule, value) => {
       validator: (_rule, value) => {
-        if (value !== formData.value.password) {
-          return Promise.reject("两次输入密码不一致");
+        if (value !== pwdForm.password) {
+          return Promise.reject(t("toast.3009"));
         }
         }
         return Promise.resolve();
         return Promise.resolve();
       },
       },
       trigger: "blur",
       trigger: "blur",
     },
     },
   ],
   ],
-};
+}));
 // ========== 修改密码弹窗表单 + 验证码倒计时 ==========
 // ========== 修改密码弹窗表单 + 验证码倒计时 ==========
 const pwdRef = ref<FormInstance>();
 const pwdRef = ref<FormInstance>();
 const codeCount = ref(0);
 const codeCount = ref(0);
@@ -282,7 +277,7 @@ const openPwdModal = () => {
 // 获取验证码倒计时
 // 获取验证码倒计时
 const getCode = () => {
 const getCode = () => {
   codeCount.value = 60;
   codeCount.value = 60;
-  message.success("验证码已发送");
+  message.success(t("information.codeSentSuccess"));
   timer = window.setInterval(() => {
   timer = window.setInterval(() => {
     codeCount.value--;
     codeCount.value--;
     if (codeCount.value <= 0) clearInterval(timer!);
     if (codeCount.value <= 0) clearInterval(timer!);
@@ -291,7 +286,7 @@ const getCode = () => {
 // 密码一致性校验
 // 密码一致性校验
 const validateSamePwd = (_rule: any, val: string) => {
 const validateSamePwd = (_rule: any, val: string) => {
   if (val && val !== pwdForm.password)
   if (val && val !== pwdForm.password)
-    return Promise.reject("两次密码输入不一致");
+    return Promise.reject(t("toast.3009"));
   return Promise.resolve();
   return Promise.resolve();
 };
 };
 // 提交密码
 // 提交密码
@@ -299,7 +294,7 @@ const submitPwd = async () => {
   await pwdRef.value?.validate();
   await pwdRef.value?.validate();
   await user.changePaswd(pwdForm);
   await user.changePaswd(pwdForm);
   pwdVisible.value = false;
   pwdVisible.value = false;
-  message.success("密码修改成功,请重新登录");
+  message.success(t("toast.24"));
   // 调用后端修改密码接口
   // 调用后端修改密码接口
 };
 };
 
 
@@ -337,7 +332,7 @@ const submitAddr = async () => {
   userInfo.name = addrForm.name;
   userInfo.name = addrForm.name;
   userInfo.mobile = addrForm.mobile;
   userInfo.mobile = addrForm.mobile;
   addrVisible.value = false;
   addrVisible.value = false;
-  message.success("地址修改成功");
+  message.success(t("information.addressChangeSuccess"));
   // 调用后端更新地址接口
   // 调用后端更新地址接口
 };
 };
 // 发送验证码
 // 发送验证码
@@ -416,4 +411,4 @@ const sendCode = async () => {
     flex: 1;
     flex: 1;
   }
   }
 }
 }
-</style>
+</style>

+ 6 - 6
src/views/information/index.vue

@@ -31,12 +31,12 @@ const cart = useCartStore();
 const activeKey = ref("0");
 const activeKey = ref("0");
 const tablist = ref([
 const tablist = ref([
   {
   {
-    label: '会员权益',
+    label: t('information.memberTable.deviceMember'),
     value: "0",
     value: "0",
     key: "0",
     key: "0",
   },
   },
   {
   {
-    label: '场景下载',
+    label: t('mall.member.sceneDownload'),
     value: "1",
     value: "1",
     key: "1",
     key: "1",
   },
   },
@@ -117,7 +117,7 @@ const hendleCooperation = () => {
     Modal.confirm({
     Modal.confirm({
         title: t('common.tip'),
         title: t('common.tip'),
         icon: createVNode(ExclamationCircleFilled),
         icon: createVNode(ExclamationCircleFilled),
-        content: '计算中/已封存的场景无法协作',
+        content: t('scene.sceneAdmin.cooperationBlockedTip'),
         centered: true,
         centered: true,
         // okText: '确认',
         // okText: '确认',
         // cancelText: '取消',
         // cancelText: '取消',
@@ -146,7 +146,7 @@ const handleEquity = (record)=> {
     Modal.confirm({
     Modal.confirm({
         title: t('common.tip'),
         title: t('common.tip'),
         icon: createVNode(ExclamationCircleFilled),
         icon: createVNode(ExclamationCircleFilled),
-        content: '确定要解除该相机的权益吗?',
+        content: t('member.unbindEquityTip'),
         centered: true,
         centered: true,
         // okText: '确认',
         // okText: '确认',
         // cancelText: '取消',
         // cancelText: '取消',
@@ -179,10 +179,10 @@ onMounted(() => {
   <div class="scene-layout">
   <div class="scene-layout">
     <div class="tab">
     <div class="tab">
       <a-tabs v-model:activeKey="activeKey" size="large" class="tablist">
       <a-tabs v-model:activeKey="activeKey" size="large" class="tablist">
-        <a-tab-pane key="0" tab="账号设置" >
+        <a-tab-pane key="0" :tab="$t('information.accountSettings')" >
           <info class="tableHeader"></info>
           <info class="tableHeader"></info>
         </a-tab-pane>
         </a-tab-pane>
-        <a-tab-pane  key="1" tab="品牌信息" >
+        <a-tab-pane  key="1" :tab="$t('information.brandInfoTab')" >
           <brandInfo />
           <brandInfo />
         </a-tab-pane>
         </a-tab-pane>
       </a-tabs>
       </a-tabs>

+ 24 - 201
src/views/mall/camera.vue

@@ -1,8 +1,8 @@
 <template>
 <template>
   <div class="shop-page">
   <div class="shop-page">
     <div class="routerInfo container" style="margin: 20px auto; font-size: 16px;">
     <div class="routerInfo container" style="margin: 20px auto; font-size: 16px;">
-      <span class="cursor-pointer" style="color: #86909C" @click="$router.go(-1)">相机配件  /</span> <span style="colot: #202020">三脚架套装</span>
-
+      <span class="cursor-pointer" style="color: #86909C" @click="$router.go(-1)">{{ t('mall.typeList.0') }}  /</span>
+      <span style="colot: #202020">{{ goodInfoData.name }}</span>
     </div>
     </div>
     <div class="shop-info">
     <div class="shop-info">
       <div class="container flex items-center justify-around" style="margin-bottom: 40px">
       <div class="container flex items-center justify-around" style="margin-bottom: 40px">
@@ -20,8 +20,6 @@
           <div class="b-label">{{ goodInfo.sub }}</div>
           <div class="b-label">{{ goodInfo.sub }}</div>
         </div>
         </div>
 
 
-        <!-- <div class="b-title">{{goodInfo.name}}</div> -->
-
         <div class="p2-con container">
         <div class="p2-con container">
           <img
           <img
             class="p2c-img"
             class="p2c-img"
@@ -47,13 +45,11 @@
           <a-col
           <a-col
             class="params-item"
             class="params-item"
             :span="12"
             :span="12"
-            v-for="(item, index) in specifications.arr"
+            v-for="(item, index) in specifications.list"
             :key="index"
             :key="index"
           >
           >
             <div class="flex">
             <div class="flex">
-              <div
-                class="p-l"
-              >
+              <div class="p-l">
                 {{ item.name }}
                 {{ item.name }}
               </div>
               </div>
               <div class="p-r">
               <div class="p-r">
@@ -63,7 +59,7 @@
           </a-col>
           </a-col>
         </a-row>
         </a-row>
         <div class="sub">
         <div class="sub">
-          <p v-for="(ites, i) in specifications.dec" :key="i">{{ ites }}</p>
+          <p>{{ specifications.dec }}</p>
         </div>
         </div>
       </div>
       </div>
       <div class="plate04">
       <div class="plate04">
@@ -97,7 +93,7 @@
           <a-col
           <a-col
             class="params-item"
             class="params-item"
             :span="12"
             :span="12"
-            v-for="(item, index) in lunguige.arr"
+            v-for="(item, index) in lunguige.list"
             :key="index"
             :key="index"
           >
           >
             <div>
             <div>
@@ -123,200 +119,27 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, reactive } from "vue";
+import { ref, reactive, computed } from "vue";
+import { useRoute } from "vue-router";
+import { useI18n } from "vue-i18n";
 import browse from "./components/browse.vue";
 import browse from "./components/browse.vue";
 import shopInfo from "./components/shopInfo.vue";
 import shopInfo from "./components/shopInfo.vue";
-// 相机配件数据源(可根据实际需求扩展)
-const browdata = ref([
-  {
-    small: "small-0",
-    big: "big-0",
-    video: true,
-  },
-  {
-    small: "small-1",
-    big: "big-1",
-  },
-  {
-    small: "small-2",
-    big: "big-2",
-  },
-  // {
-  //   small: 'small-3',
-  //   big: 'big-3'
-  // },
-  {
-    small: "small-4",
-    big: "big-4",
-  },
-]);
-const shopInfoData = reactive({
-  name: "三脚架套装",
-  price: 199,
-  color: 'black',
-  colorList: [
-    {name: '黑色', value: 'black'},
-  ],
-  showNumber: true,
-  partslist: ['三脚架型号:ET1004三脚架', '云台型号:Y-10云台', '脚轮型号:B900脚轮'],
-});
-const goodInfo = reactive({
-  name: "专业拍摄脚架",
-  sub: "采用铝合金材质锻造,兼顾轻便性与可靠性",
-  detail: "专业便携脚架细节",
-  detailArr: [
-    {
-      name: "专业球形云台",
-      img: `images/zhijia-detail01.png`,
-    },
-    {
-      name: "脚管三段角度调节按钮",
-      img: `images/zhijia-detail02.png`,
-    },
-    {
-      name: "安全的中轴升降锁紧按钮",
-      img: `images/zhijia-detail03.png`,
-    },
-    {
-      name: "防滑保暖致密海绵护手",
-      img: `images/zhijia-detail04.png`,
-    },
-    {
-      name: "可伸缩中轴挂钩稳定脚架",
-      img: `images/zhijia-detail05.png`,
-    },
-    {
-      name: "方便快捷的版扣脚管",
-      img: `images/zhijia-detail06.png`,
-    },
-  ],
-});
-const lunguige = reactive({
-  name: "技术规格",
-  dec: "注:本页面所列配置及参数均以实际上市产品为准,如有变更,恕不另行通知",
-  arr: [
-    {
-      name: "品牌型号",
-      val: ["B900脚轮"],
-    },
-    {
-      name: "脚管最小长度",
-      val: ["30cm"],
-    },
-    {
-      name: "脚管最大长度",
-      val: ["43cm"],
-    },
-    {
-      name: "脚管最短距离",
-      val: ["56cm"],
-    },
-    {
-      name: "脚管最长距离",
-      val: ["78cm"],
-    },
-    {
-      name: "收纳长度",
-      val: ["43cm"],
-    },
-    {
-      name: "材 料",
-      val: ["6063T5铝合金ABS"],
-    },
-    {
-      name: "颜 色",
-      val: ["黑色"],
-    },
-    {
-      name: "承担能力",
-      val: ["15kg"],
-    },
-    {
-      name: "脚管节数",
-      val: ["2节"],
-    },
-    {
-      name: "自 重",
-      val: ["1.7kg"],
-    },
-    {
-      name: "解锁类型",
-      val: ["板扣式"],
-    },
-    {
-      name: "滑轮直径",
-      val: ["5cm"],
-    },
-    {
-      name: "配 置",
-      val: ["脚轮、便携包"],
-    },
-  ],
-});
+import { partsData } from "./data.ts";
+
+const { t, locale } = useI18n();
+const route = useRoute();
+const language = computed(() => locale.value);
+const id = String(route.params.id || "7");
+const goodInfoData = partsData[id] || partsData[7];
+const browdata = ref(goodInfoData.browdata || []);
+const shopInfoData = reactive({ ...goodInfoData.shopInfo });
+const goodInfo = reactive(goodInfoData.goodInfo || {});
+const specifications = reactive(goodInfoData.goodInfo?.techSpecs || {});
 const hualun = reactive({
 const hualun = reactive({
-  name: "专业脚架滑轮",
-  sub: "稳固可靠 操作顺畅",
-});
-const specifications = reactive({
-  name: "技术规格",
-  dec: [
-    "*由于受环境光、显示器色差、闪光等拍摄的影响,产品颜色上可能存在一定的彩色偏差,请见谅。",
-  ],
-  arr: [
-    {
-      name: "产品型号",
-      val: ["ET1004三脚架+Y-10云台"],
-    },
-    {
-      name: "产品名称",
-      val: ["Y-10系列铝合金可反折扣式套餐"],
-    },
-    {
-      name: "产品重量",
-      val: ["1.59KG(云台重量0.37KG)"],
-    },
-    {
-      name: "云台类型",
-      val: ["Y-10专业球形云台"],
-    },
-    {
-      name: "脚架材质",
-      val: ["铝合金脚管+铝合金锻造主体"],
-    },
-    {
-      name: "脚管管径",
-      val: ["粗25mm、细16mm"],
-    },
-    {
-      name: "脚管节数",
-      val: ["4节"],
-    },
-    {
-      name: "安全负重",
-      val: ["8KG"],
-    },
-    {
-      name: "收纳高度",
-      val: ["56cm"],
-    },
-    {
-      name: "低角度",
-      val: ["21.5cm"],
-    },
-    {
-      name: "展开高度",
-      val: [
-        "不升中轴高123.5cm、升中高147cm、升中轴+短中轴高度156.5cm(含云台、云台高度8.8cm)",
-      ],
-    },
-    {
-      name: "原厂标配",
-      val: [
-        "包装盒、脚架包、脚架套装、短中轴、脚架包背带、4只六角扳手、合格证、说明书",
-      ],
-    },
-  ],
+  name: goodInfoData.goodInfo1?.name || "",
+  sub: goodInfoData.goodInfo1?.sub || "",
 });
 });
+const lunguige = reactive(goodInfoData.goodInfo1?.techSpecs || {});
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>
@@ -394,4 +217,4 @@ const specifications = reactive({
   cursor: pointer;
   cursor: pointer;
   font-size: 14px;
   font-size: 14px;
 }
 }
-</style>
+</style>

+ 21 - 13
src/views/mall/cart.vue

@@ -1,10 +1,10 @@
 <template>
 <template>
-  <div class="cart-wrap container">
+  <div class="container cart-wrap">
     <!-- 头部区域 -->
     <!-- 头部区域 -->
     <div class="cart-header">
     <div class="cart-header">
       <div class="header-left">
       <div class="header-left">
-        <h2 class="cart-title">购物车</h2>
-        <span class="tip-text">本订单顺丰包邮</span>
+        <h2 class="cart-title">{{ t('mall.cartTitle') }}</h2>
+        <span class="tip-text">{{ t('mall.cartTitleTip') }}</span>
       </div>
       </div>
       <!-- 步骤条 -->
       <!-- 步骤条 -->
       <div class="step-bar">
       <div class="step-bar">
@@ -20,13 +20,11 @@
     <div class="divider"></div>
     <div class="divider"></div>
 
 
     <!-- 空购物车 -->
     <!-- 空购物车 -->
-    <div v-if="cartlist.length === 0" class="empty-cart">
-      <img src="@/assets/image/empty.png" alt="空购物车" class="empty-img" />
-      <p class="empty-text">您的购物车空空如也</p>
-      <p class="empty-tip">赶紧行动吧</p>
-      <a-button type="primary" class="buy-btn" @click="goShop"
-        >立即购买</a-button
-      >
+    <div v-if="cartList.length === 0" class="empty-cart">
+      <img style="width: 150px" src="@/assets/image/empty.png" :alt="t('mall.cartTitle')" class="empty-img" />
+      <p class="empty-text">{{ t('mall.emptyCartText') }}</p>
+      <p class="empty-tip">{{ t('mall.emptyCartTip') }}</p>
+      <a-button type="primary" class="buy-btn" @click="goShop">{{ t('mall.nowBuy') }}</a-button>
     </div>
     </div>
 
 
     <!-- 有商品购物车列表 -->
     <!-- 有商品购物车列表 -->
@@ -38,20 +36,30 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, computed, watch } from "vue";
+import { ref, computed } from "vue";
 import { useCartStore } from "@/stores";
 import { useCartStore } from "@/stores";
 import { useRouter } from "vue-router";
 import { useRouter } from "vue-router";
-import number from "./components/number.vue";
+import { useI18n } from "vue-i18n";
 import cartlist from "./components/cart/list.vue";
 import cartlist from "./components/cart/list.vue";
 import confirmorder from "./components/cart/confirmOrder.vue";
 import confirmorder from "./components/cart/confirmOrder.vue";
-import { stepList } from "./data";
 
 
 defineOptions({ name: "MallCart" });
 defineOptions({ name: "MallCart" });
 
 
+const { t } = useI18n();
 const router = useRouter();
 const router = useRouter();
 const cart = useCartStore();
 const cart = useCartStore();
 const step = ref(1);
 const step = ref(1);
 const cartList = computed(() => cart.getList);
 const cartList = computed(() => cart.getList);
+
+const stepList = computed(() => [
+  { id: 1, name: t('mall.addBuy') },
+  { id: 2, name: t('mall.confirmOrder') },
+  { id: 3, name: t('mall.submitOrder') },
+]);
+
+const goShop = () => {
+  router.push('/mall/index');
+};
 </script>
 </script>
 
 
 <style lang="less" scoped>
 <style lang="less" scoped>

+ 5 - 2
src/views/mall/components/Parts.vue

@@ -1,7 +1,7 @@
 <template>
 <template>
   <div class="camera-accessories container ">
   <div class="camera-accessories container ">
     <!-- 标题 -->
     <!-- 标题 -->
-    <div class="accessories-title">相机配件</div>
+    <div class="accessories-title">{{ t('mall.accessoriesTitle') }}</div>
     
     
     <!-- 配件列表:v-for 遍历 -->
     <!-- 配件列表:v-for 遍历 -->
     <div class="accessories-list">
     <div class="accessories-list">
@@ -24,7 +24,7 @@
         <div class="accessories-info w-full flex justify-between items-center">
         <div class="accessories-info w-full flex justify-between items-center">
           <div class="accessories-price">¥{{ item.price }}</div>
           <div class="accessories-price">¥{{ item.price }}</div>
           <!-- 购买按钮 -->
           <!-- 购买按钮 -->
-          <button class="buy-btn">立即购买</button>
+          <button class="buy-btn">{{ t('mall.nowBuy') }}</button>
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
@@ -33,11 +33,14 @@
 
 
 <script setup>
 <script setup>
 import { ref } from 'vue'
 import { ref } from 'vue'
+import { useI18n } from 'vue-i18n'
 import usb from '@/assets/image/mall/usb.png'
 import usb from '@/assets/image/mall/usb.png'
 import battery from '@/assets/image/mall/battery.png'
 import battery from '@/assets/image/mall/battery.png'
 import tripodSet from '@/assets/image/mall/tripod-set.png'
 import tripodSet from '@/assets/image/mall/tripod-set.png'
 import  { partsData } from '../data.ts'
 import  { partsData } from '../data.ts'
 
 
+const { t } = useI18n()
+
 // 相机配件数据源(可根据实际需求扩展)
 // 相机配件数据源(可根据实际需求扩展)
 const accessoriesList = ref([
 const accessoriesList = ref([
   {
   {

+ 2 - 2
src/views/mall/components/cart/confirmOrder.vue

@@ -16,7 +16,7 @@
 
 
         <div class="addr-desc">{{ receiverInfo.shipAddress }}</div>
         <div class="addr-desc">{{ receiverInfo.shipAddress }}</div>
 
 
-        <span class="cursor-pointer edit-text" @click="handleEditAddr">{{ t('manage.account.edit') }}</span>
+        <span class="cursor-pointer edit-text" @click="handleEditAddr">{{ t('mall.account.edit') }}</span>
 
 
       </div>
       </div>
 
 
@@ -330,7 +330,7 @@ const handlePay = async () => {
 
 
   if (isZh.value && !agreeProtocol.value) {
   if (isZh.value && !agreeProtocol.value) {
 
 
-    message.warn(t('toast.agreeXieyi'))
+    message.warn(t('login.agreeXieyi'))
 
 
     return
     return
 
 

+ 10 - 13
src/views/mall/components/cart/list.vue

@@ -5,10 +5,10 @@
         <div class="col col-check">
         <div class="col col-check">
           <a-checkbox v-model:checked="checkedAll" />
           <a-checkbox v-model:checked="checkedAll" />
         </div>
         </div>
-        <div class="col col-goods">商品</div>
-        <div class="col col-price">价格</div>
-        <div class="col col-num">数量</div>
-        <div class="col col-sub">小计</div>
+        <div class="col col-goods">{{ t('mall.goods') }}</div>
+        <div class="col col-price">{{ t('mall.price') }}</div>
+        <div class="col col-num">{{ t('mall.count') }}</div>
+        <div class="col col-sub">{{ t('mall.mini') }}</div>
         <div class="col col-del"></div>
         <div class="col col-del"></div>
       </div>
       </div>
       <div class="divider-sm"></div>
       <div class="divider-sm"></div>
@@ -45,7 +45,7 @@
       <div class="cart-footer-top">
       <div class="cart-footer-top">
         <div></div>
         <div></div>
         <div class="total-info">
         <div class="total-info">
-          <span>共计{{ selectedCount }}件商品</span>
+          <span>{{ t('mall.countNum', { num: selectedCount }) }}</span>
           <span class="total-price">¥{{ totalMoney }}</span>
           <span class="total-price">¥{{ totalMoney }}</span>
         </div>
         </div>
       </div>
       </div>
@@ -55,26 +55,23 @@
         <div class="pay-icon">
         <div class="pay-icon">
           <img
           <img
             src="@/assets/image/mall/ali-pay.png"
             src="@/assets/image/mall/ali-pay.png"
-            alt="支付宝"
+            :alt="t('mall.aliPay')"
             class="pay-alipay"
             class="pay-alipay"
           />
           />
           <img
           <img
             src="@/assets/image/mall/wechat-pay.png"
             src="@/assets/image/mall/wechat-pay.png"
-            alt="微信支付"
+            :alt="t('mall.wechatPay')"
             class="pay-wechat"
             class="pay-wechat"
           />
           />
         </div>
         </div>
         <div class="btn-group">
         <div class="btn-group">
-          <a-button size="large" class="cancel-btn" @click="goShop"
-            >继续选购</a-button
-          >
+          <a-button size="large" class="cancel-btn" @click="goShop">{{ t('mall.continueBuy') }}</a-button>
           <a-button
           <a-button
             type="primary"
             type="primary"
             size="large"
             size="large"
             class="settle-btn"
             class="settle-btn"
             @click="goConfirm"
             @click="goConfirm"
-            >立即结算</a-button
-          >
+          >{{ t('mall.nowJieSuan') }}</a-button>
         </div>
         </div>
       </div>
       </div>
   </div>
   </div>
@@ -143,7 +140,7 @@ const handleRemove = (item) => {
 
 
 // 去选购
 // 去选购
 const goShop = () => {
 const goShop = () => {
-  router.push("/goods");
+  router.push("/mall/index");
 };
 };
 
 
 // 去确认订单
 // 去确认订单

+ 11 - 6
src/views/mall/components/header.vue

@@ -20,6 +20,7 @@ const isLogin = computed(() => userStore.token);
 const cartCount = computed(() => cart.getCartCount);
 const cartCount = computed(() => cart.getCartCount);
 const { t } = useI18n();
 const { t } = useI18n();
 const router = useRouter()
 const router = useRouter()
+const pathname = ref(import.meta.env.VITE_PATH || window.location.pathname)
 const visible = ref(false);
 const visible = ref(false);
 const handleMenuClick = (item) => {
 const handleMenuClick = (item) => {
   console.log(item);
   console.log(item);
@@ -28,6 +29,10 @@ const handleMenuClick = (item) => {
 const handleCartClick = (item) => {
 const handleCartClick = (item) => {
   console.log(item);
   console.log(item);
 };
 };
+const handleContactUs = (item) => {
+  window.open(`${pathname.value}#/contactUs`, '_blank');
+};
+
 const handleUserClick = (e) => {
 const handleUserClick = (e) => {
   if (e.key === "1") {
   if (e.key === "1") {
     // 跳转到账户信息页面
     // 跳转到账户信息页面
@@ -54,7 +59,7 @@ const handleUserClick = (e) => {
 <template>
 <template>
   <div class="header">
   <div class="header">
     <div
     <div
-      class="container account-header flex items-center justify-between h-20"
+      class="container flex items-center justify-between h-20 account-header"
     >
     >
       <img
       <img
       @click="$router.push('/home')"
       @click="$router.push('/home')"
@@ -65,7 +70,7 @@ const handleUserClick = (e) => {
         "
         "
         alt
         alt
       />
       />
-      <div class="headerRight flex items-center gap-4">
+      <div class="flex items-center gap-4 headerRight">
         <a-dropdown v-model:open="visible" arrow placement="bottom">
         <a-dropdown v-model:open="visible" arrow placement="bottom">
           <div class="lang">
           <div class="lang">
             <GlobalOutlined style="font-size: 18px; margin-right: 8px" />
             <GlobalOutlined style="font-size: 18px; margin-right: 8px" />
@@ -92,7 +97,7 @@ const handleUserClick = (e) => {
         </a-dropdown>
         </a-dropdown>
 
 
         <div
         <div
-          class="haeder-right-item cursor-pointer haeder-right-head user"
+          class="cursor-pointer haeder-right-item haeder-right-head user"
           :class="{ vip: userStore.incrementNum }"
           :class="{ vip: userStore.incrementNum }"
           :style="{ 'background-image': `url(${userStore.head})` }"
           :style="{ 'background-image': `url(${userStore.head})` }"
           @click="$router.push('/information')"
           @click="$router.push('/information')"
@@ -111,12 +116,12 @@ const handleUserClick = (e) => {
           </div>
           </div>
           <template #overlay>
           <template #overlay>
             <a-menu @click="handleUserClick">
             <a-menu @click="handleUserClick">
-              <a-menu-item key="1">我的账户 </a-menu-item>
-              <a-menu-item key="2">退出登录 </a-menu-item>
+              <a-menu-item key="1">{{ t('mall.myAccount') }}</a-menu-item>
+              <a-menu-item key="2">{{ t('mall.logOut') }}</a-menu-item>
             </a-menu>
             </a-menu>
           </template>
           </template>
         </a-dropdown>
         </a-dropdown>
-        <div class="contactUs cursor-pointer">联系我们</div>
+        <div @click="handleContactUs" class="cursor-pointer contactUs">{{ t('mall.contactUs') }}</div>
 
 
         <div class="cart" @click.stop="$router.push({ name: 'mallCart' })">
         <div class="cart" @click.stop="$router.push({ name: 'mallCart' })">
           <icon-font type="icon-xingouwuche" style="font-size: 26px;" />
           <icon-font type="icon-xingouwuche" style="font-size: 26px;" />

+ 10 - 8
src/views/mall/components/shopInfo.vue

@@ -5,24 +5,24 @@
     <div class="product-price">RMB {{shopInfo.price}}</div>
     <div class="product-price">RMB {{shopInfo.price}}</div>
     
     
     <div class="section-item" v-if="shopInfo.colorList && shopInfo.colorList.length">
     <div class="section-item" v-if="shopInfo.colorList && shopInfo.colorList.length">
-      <div class="section-label">颜色</div>
+      <div class="section-label">{{ t('mall.color') }}</div>
       <a-radio-group v-model:value="form.color" class="color-radio-group">
       <a-radio-group v-model:value="form.color" class="color-radio-group">
         <a-radio v-for="item, index in shopInfo.colorList" :key="index" :value="item.value">{{item.name}}</a-radio>
         <a-radio v-for="item, index in shopInfo.colorList" :key="index" :value="item.value">{{item.name}}</a-radio>
       </a-radio-group>
       </a-radio-group>
     </div>
     </div>
 
 
     <div class="section-item" v-if="shopInfo.partslist && shopInfo.partslist.length">
     <div class="section-item" v-if="shopInfo.partslist && shopInfo.partslist.length">
-      <div class="section-label">配件</div>
+      <div class="section-label">{{ t('mall.accessory') }}</div>
       <ul class="accessory-list">
       <ul class="accessory-list">
         <li v-for="item, index in shopInfo.partslist" :key="index">{{item}}</li>
         <li v-for="item, index in shopInfo.partslist" :key="index">{{item}}</li>
       </ul>
       </ul>
     </div>
     </div>
     <div class="section-item" v-if="shopInfo.introduction">
     <div class="section-item" v-if="shopInfo.introduction">
-      <div class="section-label">简介</div>
+      <div class="section-label">{{ t('mall.intro') }}</div>
       <div>{{shopInfo.introduction}}</div>
       <div>{{shopInfo.introduction}}</div>
     </div>
     </div>
     <div class="section-item quantity-section" v-if="shopInfo.showNumber">
     <div class="section-item quantity-section" v-if="shopInfo.showNumber">
-      <span class="section-label">数量</span>
+      <span class="section-label">{{ t('mall.count') }}</span>
       <number class="custom-number-input" v-model="form.quantity" :max="99" />
       <number class="custom-number-input" v-model="form.quantity" :max="99" />
     </div>
     </div>
 
 
@@ -34,7 +34,7 @@
         :loading="loading"
         :loading="loading"
         @click="handleAddToCart"
         @click="handleAddToCart"
       >
       >
-        {{shopInfo.okText||'加入购物车'}}
+        {{ shopInfo.okText || t('mall.cart') }}
       </a-button>
       </a-button>
     </div>
     </div>
 
 
@@ -42,16 +42,18 @@
       <span class="icon-truck">
       <span class="icon-truck">
         <CarOutlined />
         <CarOutlined />
       </span>
       </span>
-      <span class="tips-text">付款成功后5个工作日内发货,默认顺丰快递包邮</span>
+      <span class="tips-text">{{ t('mall.shippingTips') }}</span>
     </div>
     </div>
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { reactive, defineEmits, watch } from 'vue'
+import { reactive, defineEmits } from 'vue'
+import { useI18n } from 'vue-i18n'
 import { CarOutlined } from '@ant-design/icons-vue'
 import { CarOutlined } from '@ant-design/icons-vue'
-import { message } from 'ant-design-vue'
 import number from './number.vue'
 import number from './number.vue'
+
+const { t } = useI18n()
 // 1. 定义 props
 // 1. 定义 props
 const props = defineProps({
 const props = defineProps({
   shopInfo: {
   shopInfo: {

+ 40 - 55
src/views/mall/components/shopMemberCard.vue

@@ -22,7 +22,7 @@
                 {{ t('mall.member.normalPrice') }}
                 {{ t('mall.member.normalPrice') }}
               </template>
               </template>
               <template v-else>
               <template v-else>
-                {{ item.price }}<span class="unit">元/年</span>
+                {{ item.price }}<span class="unit">{{ t('mall.unitPerYear') }}</span>
               </template>
               </template>
             </div>
             </div>
             <p class="card-desc">{{ getMemberDesc(item.type) }}</p>
             <p class="card-desc">{{ getMemberDesc(item.type) }}</p>
@@ -46,13 +46,13 @@
               :type="item.type === 'freeMember' ? 'default' : 'primary'"
               :type="item.type === 'freeMember' ? 'default' : 'primary'"
               @click="handleBuy(item)"
               @click="handleBuy(item)"
             >
             >
-              {{ item.type === 'freeMember' ? '立即体验' : '立即购买' }}
+              {{ item.type === 'freeMember' ? t('mall.tryNow') : t('mall.buyNow') }}
             </a-button>
             </a-button>
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
 
 
-      <h2 class="section-title">常见问题</h2>
+      <h2 class="section-title">{{ t('mall.faqTitle') }}</h2>
       <div v-for="item in faqList" :key="item.key" class="faq-panel">
       <div v-for="item in faqList" :key="item.key" class="faq-panel">
         <div class="faq-header">{{ item.header }}</div>
         <div class="faq-header">{{ item.header }}</div>
         <div class="faq-content">
         <div class="faq-content">
@@ -98,46 +98,6 @@ const MEMBER_CARD_CLASS: Record<MemberType, string> = {
   member: 'seMember',
   member: 'seMember',
 }
 }
 
 
-interface MemberConfig {
-  title: string
-  desc: string
-  cameraType: string
-  capacity: string
-  brandSupport: string
-  priorityCal: string
-  downloadText: (item: EquityItem) => string
-}
-
-const MEMBER_CONFIG: Record<MemberType, MemberConfig> = {
-  freeMember: {
-    title: '普通权益',
-    desc: '具备基础的场景编辑功能及有限云容量,适用于个人爱好者,或无强定制信息需求的团队。',
-    cameraType: '激光+全景',
-    capacity: '10GB',
-    brandSupport: '不支持',
-    priorityCal: '根据实际情况排队',
-    downloadText: (item) => `${item.downloadPrice}元/次`,
-  },
-  seMember: {
-    title: '高级会员',
-    desc: '适用于品牌宣传要求较高,灵活定制品牌信息,对于云空间容量灵活控制的专业用户或企业。',
-    cameraType: '全景',
-    capacity: '无限容量',
-    brandSupport: '支持',
-    priorityCal: '免排队',
-    downloadText: () => '免费10次',
-  },
-  member: {
-    title: '专业会员',
-    desc: '适用于专业要求较高,主要用于测量测绘,工程应用等企业级用户。',
-    cameraType: '激光',
-    capacity: '无限容量',
-    brandSupport: '支持',
-    priorityCal: '免排队',
-    downloadText: () => '免费10次',
-  },
-}
-
 const FAQ_KEYS = ['1', '2', '3', '4', '5', '6'] as const
 const FAQ_KEYS = ['1', '2', '3', '4', '5', '6'] as const
 
 
 const faqList = computed(() =>
 const faqList = computed(() =>
@@ -150,32 +110,57 @@ const faqList = computed(() =>
 
 
 const getMemberCardClass = (type: MemberType) => MEMBER_CARD_CLASS[type]
 const getMemberCardClass = (type: MemberType) => MEMBER_CARD_CLASS[type]
 
 
-const getMemberTitle = (type: MemberType) => MEMBER_CONFIG[type].title
+const getMemberTitle = (type: MemberType) => {
+  if (type === 'freeMember') return t('mall.member.normal')
+  if (type === 'seMember') return t('member.Senior')
+  return t('member.Premium')
+}
 
 
-const getMemberDesc = (type: MemberType) => MEMBER_CONFIG[type].desc
+const getMemberDesc = (type: MemberType) => {
+  if (type === 'freeMember') return t('mall.member.normalIntro')
+  if (type === 'seMember') return t('mall.member.privilegeIntro')
+  return t('member.js.0')
+}
 
 
 const getMemberFeatures = (item: EquityItem): FeatureRow[] => {
 const getMemberFeatures = (item: EquityItem): FeatureRow[] => {
-  const config = MEMBER_CONFIG[item.type]
+  const type = item.type
+  const capacity =
+    type === 'freeMember' ? t('mall.member.normalCapacity') : t('mall.member.privilegeCapacity')
+  const brandSupport =
+    type === 'freeMember' ? t('mall.member.notSupport') : t('mall.member.support')
+  const priorityCal =
+    type === 'freeMember' ? t('mall.member.normalCal') : t('mall.member.privilegeCal')
+  const downloadText =
+    type === 'freeMember'
+      ? t('mall.pricePerTime', { price: item.downloadPrice })
+      : t('mall.freeDownloadCount')
+  const cameraType =
+    type === 'freeMember'
+      ? t('mall.cameraLaserPanorama')
+      : type === 'seMember'
+        ? t('mall.cameraPanorama')
+        : t('mall.cameraLaser')
+
   return [
   return [
     {
     {
       label: t('scene.sceneAdmin.filterFormModule.cameraType'),
       label: t('scene.sceneAdmin.filterFormModule.cameraType'),
-      value: config.cameraType,
+      value: cameraType,
     },
     },
     {
     {
-      label: '云容量',
-      value: config.capacity,
+      label: t('mall.featureCapacity'),
+      value: capacity,
     },
     },
     {
     {
-      label: '品牌信息',
-      value: config.brandSupport,
+      label: t('mall.featureBrand'),
+      value: brandSupport,
     },
     },
     {
     {
-      label: '优先计算',
-      value: config.priorityCal,
+      label: t('mall.featurePriority'),
+      value: priorityCal,
     },
     },
     {
     {
-      label: '离线包下载',
-      value: config.downloadText(item),
+      label: t('mall.featureDownload'),
+      value: downloadText,
     },
     },
   ]
   ]
 }
 }

+ 19 - 3
src/views/mall/components/valueAdded.vue

@@ -1,11 +1,11 @@
 <template>
 <template>
   <div class="container service-wrap">
   <div class="container service-wrap">
-    <h2 class="service-title">增值服务</h2>
+    <h2 class="service-title">{{ t('mall.valueAddedTitle') }}</h2>
     <div class="service-list">
     <div class="service-list">
       <div
       <div
         v-for="item in serviceList"
         v-for="item in serviceList"
         :key="item.id"
         :key="item.id"
-        @click="$router.push(`/mall/sercice/${item.id}`)"
+        @click="handleItem(item)"
         class="service-card"
         class="service-card"
       >
       >
         <!-- 图标+标题 -->
         <!-- 图标+标题 -->
@@ -29,13 +29,29 @@
 
 
 <script setup>
 <script setup>
 import { ref, onMounted } from 'vue'
 import { ref, onMounted } from 'vue'
+import { useI18n } from 'vue-i18n'
+import { message } from "ant-design-vue";
 import { serviceConfigList, userSerciceList } from '@/api/mall'
 import { serviceConfigList, userSerciceList } from '@/api/mall'
-import { serciceData, filterServiceMenuList } from '../data'
+import { serciceData } from '../data'
+import { useRouter, useRoute } from 'vue-router'
+const router = useRouter()
+
+const { t } = useI18n()
 // 增值服务数据源
 // 增值服务数据源
 const serviceList = ref([])
 const serviceList = ref([])
 const getList = async () => {
 const getList = async () => {
   serviceList.value = await serviceConfigList()
   serviceList.value = await serviceConfigList()
 }
 }
+const handleItem = async (item) => {
+  const userServiceList = await userSerciceList();
+  let userItem = userServiceList.find(ele => ele.id == item.id)
+  if (userItem) {
+    message.info(t('mall.paySuccessYouHave'));
+    return;
+  }
+
+  router.push(`/mall/sercice/${item.id}`)
+}
 onMounted(getList)
 onMounted(getList)
 </script>
 </script>
 
 

+ 142 - 436
src/views/mall/data.ts

@@ -1,7 +1,6 @@
 import { ref } from "vue";
 import { ref } from "vue";
 import i18n from "@/locales/index.js";
 import i18n from "@/locales/index.js";
 function $t(key: string) {
 function $t(key: string) {
-  console.log(i18n, 'global');
   return i18n.global.t(key);
   return i18n.global.t(key);
 }
 }
 export enum goodsType {
 export enum goodsType {
@@ -15,231 +14,108 @@ const usb = 999
 const battery = 1999
 const battery = 1999
 const type = 1
 const type = 1
 
 
+const tripodDetailArr = () => [
+  { name: $t('mall.tripod.featureBallHead'), img: `images/zhijia-detail01.png` },
+  { name: $t('mall.tripod.featureLegAngle'), img: `images/zhijia-detail02.png` },
+  { name: $t('mall.tripod.featureCenterLock'), img: `images/zhijia-detail03.png` },
+  { name: $t('mall.tripod.featureGrip'), img: `images/zhijia-detail04.png` },
+  { name: $t('mall.tripod.featureHook'), img: `images/zhijia-detail05.png` },
+  { name: $t('mall.tripod.featureLegLock'), img: `images/zhijia-detail06.png` },
+]
+
+const tripodMainSpecs = () => ({
+  name: $t('mall.spec.title'),
+  dec: $t('mall.spec.colorNote'),
+  list: [
+    { name: $t('mall.spec.productModel'), val: ["ET1004三脚架+Y-10云台"] },
+    { name: $t('mall.spec.productName'), val: ["Y-10系列铝合金可反折扣式套餐"] },
+    { name: $t('mall.spec.productWeight'), val: ["1.59KG(云台重量0.37KG)"] },
+    { name: $t('mall.spec.gimbalType'), val: ["Y-10专业球形云台"] },
+    { name: $t('mall.spec.legMaterial'), val: ["铝合金脚管+铝合金锻造主体"] },
+    { name: $t('mall.spec.legDiameter'), val: ["粗25mm、细16mm"] },
+    { name: $t('mall.spec.legSections'), val: ["4节"] },
+    { name: $t('mall.spec.safeLoad'), val: ["8KG"] },
+    { name: $t('mall.spec.storageHeight'), val: ["56cm"] },
+    { name: $t('mall.spec.lowAngle'), val: ["21.5cm"] },
+    {
+      name: $t('mall.spec.extendedHeight'),
+      val: ["不升中轴高123.5cm、升中高147cm、升中轴+短中轴高度156.5cm(含云台、云台高度8.8cm)"],
+    },
+    {
+      name: $t('mall.spec.standardAccessories'),
+      val: ["包装盒、脚架包、脚架套装、短中轴、脚架包背带、4只六角扳手、合格证、说明书"],
+    },
+  ],
+})
+
+const tripodCasterSpecs = () => ({
+  name: $t('mall.spec.title'),
+  dec: $t('mall.spec.disclaimer'),
+  list: [
+    { name: $t('mall.spec.brandModel'), val: ["B900脚轮"] },
+    { name: $t('mall.spec.legMinLength'), val: ["30cm"] },
+    { name: $t('mall.spec.legMaxLength'), val: ["43cm"] },
+    { name: $t('mall.spec.legMinDistance'), val: ["56cm"] },
+    { name: $t('mall.spec.legMaxDistance'), val: ["78cm"] },
+    { name: $t('mall.spec.storageLength'), val: ["43cm"] },
+    { name: $t('mall.spec.material'), val: ["6063T5铝合金ABS"] },
+    { name: $t('mall.spec.color'), val: [$t('mall.black')] },
+    { name: $t('mall.spec.loadCapacity'), val: ["15kg"] },
+    { name: $t('mall.spec.legSections'), val: ["2节"] },
+    { name: $t('mall.spec.selfWeight'), val: ["1.7kg"] },
+    { name: $t('mall.spec.unlockType'), val: ["板扣式"] },
+    { name: $t('mall.spec.wheelDiameter'), val: ["5cm"] },
+    { name: $t('mall.spec.configuration'), val: ["脚轮、便携包"] },
+  ],
+})
+
 export const partsData = {
 export const partsData = {
   4: {
   4: {
     type: goodsType.camera,
     type: goodsType.camera,
-    name: '四维看看 八目相机',
+    name: $t('mall.kankanPro'),
     img: `images/banner_pro.png`,
     img: `images/banner_pro.png`,
-    detail: ['四维看看 八目相机(静谧黑)', '容量套餐(10G)'],
+    detail: [`${$t('mall.kankanPro')}(${$t('mall.blackColor')})`, $t('mall.voice')],
     price: price,
     price: price,
     skuSn: 'U15609161635760015'
     skuSn: 'U15609161635760015'
   },
   },
   7: {
   7: {
     type: goodsType.parts,
     type: goodsType.parts,
-    name: '三脚架套装',
+    name: $t('header.tripod'),
     img: `images/zhijia.png`,
     img: `images/zhijia.png`,
-    detail: ['四维看看精选三脚架套装(标准色)'],
+    detail: [`${$t('mall.parts')}(${$t('mall.zhijiaColor')})`],
     price: zhijiaPrice,
     price: zhijiaPrice,
     skuSn: 'U15604134406280073',
     skuSn: 'U15604134406280073',
     browdata: [
     browdata: [
-      {
-        small: "small-0",
-        big: "big-0",
-        video: true,
-      },
-      {
-        small: "small-1",
-        big: "big-1",
-      },
-      {
-        small: "small-2",
-        big: "big-2",
-      },
-      {
-        small: "small-4",
-        big: "big-4",
-      }],
+      { small: "small-0", big: "big-0", video: true },
+      { small: "small-1", big: "big-1" },
+      { small: "small-2", big: "big-2" },
+      { small: "small-4", big: "big-4" },
+    ],
     shopInfo: {
     shopInfo: {
-      name: "四维看看精选三脚架套装",
+      name: $t('mall.parts'),
       price: 199,
       price: 199,
       color: 'black',
       color: 'black',
-      colorList: [
-        { name: '黑色', value: 'black' },
-      ],
+      colorList: [{ name: $t('mall.black'), value: 'black' }],
       showNumber: true,
       showNumber: true,
-      partslist: ['三脚架型号:ET1004三脚架', '云台型号:Y-10云台', '脚轮型号:B900脚轮'],
+      partslist: [
+        $t('mall.tripod.tripodModel'),
+        $t('mall.tripod.gimbalModel'),
+        $t('mall.tripod.casterModel'),
+      ],
     },
     },
     goodInfo: {
     goodInfo: {
-      name: "专业拍摄脚架",
-      sub: "采用铝合金材质锻造,兼顾轻便性与可靠性",
-      detail: "专业便携脚架细节",
-      detailArr: [
-        {
-          name: "专业球形云台",
-          img: `images/zhijia-detail01.png`,
-        },
-        {
-          name: "脚管三段角度调节按钮",
-          img: `images/zhijia-detail02.png`,
-        },
-        {
-          name: "安全的中轴升降锁紧按钮",
-          img: `images/zhijia-detail03.png`,
-        },
-        {
-          name: "防滑保暖致密海绵护手",
-          img: `images/zhijia-detail04.png`,
-        },
-        {
-          name: "可伸缩中轴挂钩稳定脚架",
-          img: `images/zhijia-detail05.png`,
-        },
-        {
-          name: "方便快捷的版扣脚管",
-          img: `images/zhijia-detail06.png`,
-        },
-      ],
-      techSpecs: {
-        name: "技术规格",
-        dec: "*由于受环境光、显示器色差、闪光等拍摄的影响,产品颜色上可能存在一定的彩色偏差,请见谅。",
-        list: [
-          {
-            name: "产品型号",
-            val: ["ET1004三脚架+Y-10云台"],
-          },
-          {
-            name: "产品名称",
-            val: ["Y-10系列铝合金可反折扣式套餐"],
-          },
-          {
-            name: "产品重量",
-            val: ["1.59KG(云台重量0.37KG)"],
-          },
-          {
-            name: "云台类型",
-            val: ["Y-10专业球形云台"],
-          },
-          {
-            name: "脚架材质",
-            val: ["铝合金脚管+铝合金锻造主体"],
-          },
-          {
-            name: "脚管管径",
-            val: ["粗25mm、细16mm"],
-          },
-          {
-            name: "脚管节数",
-            val: ["4节"],
-          },
-          {
-            name: "安全负重",
-            val: ["8KG"],
-          },
-          {
-            name: "收纳高度",
-            val: ["56cm"],
-          },
-          {
-            name: "低角度",
-            val: ["21.5cm"],
-          },
-          {
-            name: "展开高度",
-            val: [
-              "不升中轴高123.5cm、升中高147cm、升中轴+短中轴高度156.5cm(含云台、云台高度8.8cm)",
-            ],
-          },
-          {
-            name: "原厂标配",
-            val: [
-              "包装盒、脚架包、脚架套装、短中轴、脚架包背带、4只六角扳手、合格证、说明书",
-            ],
-          },
-        ],
-      }
+      name: $t('mall.tripod.proTripod'),
+      sub: $t('mall.tripod.proTripodSub'),
+      detail: $t('mall.tripod.detailTitle'),
+      detailArr: tripodDetailArr(),
+      techSpecs: tripodMainSpecs(),
     },
     },
     goodInfo1: {
     goodInfo1: {
-      name: "专业脚架滑轮",
-      sub: "稳固可靠 操作顺畅",
-      detail: "专业便携脚架细节",
-      detailArr: [
-        {
-          name: "专业球形云台",
-          img: `images/zhijia-detail01.png`,
-        },
-        {
-          name: "脚管三段角度调节按钮",
-          img: `images/zhijia-detail02.png`,
-        },
-        {
-          name: "安全的中轴升降锁紧按钮",
-          img: `images/zhijia-detail03.png`,
-        },
-        {
-          name: "防滑保暖致密海绵护手",
-          img: `images/zhijia-detail04.png`,
-        },
-        {
-          name: "可伸缩中轴挂钩稳定脚架",
-          img: `images/zhijia-detail05.png`,
-        },
-        {
-          name: "方便快捷的版扣脚管",
-          img: `images/zhijia-detail06.png`,
-        },
-      ],
-      techSpecs: {
-        name: "技术规格",
-        dec: "注:本页面所列配置及参数均以实际上市产品为准,如有变更,恕不另行通知",
-        list: [
-          {
-            name: "品牌型号",
-            val: ["B900脚轮"],
-          },
-          {
-            name: "脚管最小长度",
-            val: ["30cm"],
-          },
-          {
-            name: "脚管最大长度",
-            val: ["43cm"],
-          },
-          {
-            name: "脚管最短距离",
-            val: ["56cm"],
-          },
-          {
-            name: "脚管最长距离",
-            val: ["78cm"],
-          },
-          {
-            name: "收纳长度",
-            val: ["43cm"],
-          },
-          {
-            name: "材 料",
-            val: ["6063T5铝合金ABS"],
-          },
-          {
-            name: "颜 色",
-            val: ["黑色"],
-          },
-          {
-            name: "承担能力",
-            val: ["15kg"],
-          },
-          {
-            name: "脚管节数",
-            val: ["2节"],
-          },
-          {
-            name: "自 重",
-            val: ["1.7kg"],
-          },
-          {
-            name: "解锁类型",
-            val: ["板扣式"],
-          },
-          {
-            name: "滑轮直径",
-            val: ["5cm"],
-          },
-          {
-            name: "配 置",
-            val: ["脚轮、便携包"],
-          },
-        ],
-      }
+      name: $t('mall.tripod.proCaster'),
+      sub: $t('mall.tripod.proCasterSub'),
+      detail: $t('mall.tripod.detailTitle'),
+      detailArr: tripodDetailArr(),
+      techSpecs: tripodCasterSpecs(),
     }
     }
   },
   },
   11: {
   11: {
@@ -249,104 +125,41 @@ export const partsData = {
     detail: [`${$t('mall.usb')}(${$t('mall.silvery')})`],
     detail: [`${$t('mall.usb')}(${$t('mall.silvery')})`],
     price: usb,
     price: usb,
     skuSn: 'U15604132404280603',
     skuSn: 'U15604132404280603',
-
     browdata: [
     browdata: [
-      {
-        small: 'usb-0',
-        big: 'usb-0',
-        // video: true
-      }, {
-        small: 'usb-1',
-        big: 'usb-1'
-      },
-      {
-        small: 'usb-2',
-        big: 'usb-2'
-      },
-      {
-        small: 'usb-3',
-        big: 'usb-3'
-      }],
+      { small: 'usb-0', big: 'usb-0' },
+      { small: 'usb-1', big: 'usb-1' },
+      { small: 'usb-2', big: 'usb-2' },
+      { small: 'usb-3', big: 'usb-3' },
+    ],
     shopInfo: {
     shopInfo: {
-      name: "四维深时高速固态 U 盘",
+      name: $t('mall.usb'),
       price: 999,
       price: 999,
-      color: '银色',
-      colorList: [
-        { name: '银色', value: '银色' },
-      ],
+      color: 'silvery',
+      colorList: [{ name: $t('mall.silvery'), value: 'silvery' }],
       showNumber: true,
       showNumber: true,
       partslist: [
       partslist: [
-        '服务:四维深时本地版专用',
-        '容量:128GB',
-        'USB3.1 Type-C 双U口',
+        $t('mall.usbParts.service'),
+        $t('mall.usbParts.capacity'),
+        $t('mall.usbParts.interface'),
       ]
       ]
     },
     },
-
     goodInfo: {
     goodInfo: {
-      name: "技术规格",
+      name: $t('mall.spec.title'),
       techSpecs: {
       techSpecs: {
-        name: '技术规格',
-        dec: '*由于受环境光、显示器色差、闪光等拍摄的影响,产品颜色上可能存在一定的彩色偏差,请见谅。',
-        list: [{
-          name: '产品尺寸',
-          val: ['69.4x20.75x8.7mm']
-        },
-        {
-          name: '产品重量',
-          val: ['38g']
-        },
-        {
-          name: '颜   色',
-          val: ['银色']
-        },
-        {
-          name: '存储介质',
-          val: [
-            'NAND Flash'
-          ]
-        },
-        {
-          name: '工作电压',
-          val: [
-            '5V±5%'
-          ]
-        },
-        {
-          name: '工作电流',
-          val: [
-            '0.5~1A'
-          ]
-        },
-        {
-          name: '操作温度',
-          val: [
-            '0~45℃'
-          ]
-        },
-        {
-          name: '保存温度',
-          val: [
-            '-25~70℃'
-          ]
-        },
-        {
-          name: '接口类型',
-          val: [
-            'USB3.1 Type-C/A'
-          ]
-        },
-        {
-          name: '速   度',
-          val: [
-            '读速可达520MB/s以上,写速可达420MB/s'
-          ]
-        },
-        {
-          name: '操作系统',
-          val: [
-            '支持Window7/8/10,MAC10.6以上'
-          ]
-        },
+        name: $t('mall.spec.title'),
+        dec: $t('mall.spec.colorNote'),
+        list: [
+          { name: $t('mall.spec.productSize'), val: ['69.4x20.75x8.7mm'] },
+          { name: $t('mall.spec.productWeight'), val: ['38g'] },
+          { name: $t('mall.spec.color'), val: [$t('mall.silvery')] },
+          { name: $t('mall.spec.storageMedium'), val: ['NAND Flash'] },
+          { name: $t('mall.spec.operatingVoltage'), val: ['5V±5%'] },
+          { name: $t('mall.spec.operatingCurrent'), val: ['0.5~1A'] },
+          { name: $t('mall.spec.operatingTemp'), val: ['0~45℃'] },
+          { name: $t('mall.spec.storageTemp'), val: ['-25~70℃'] },
+          { name: $t('mall.spec.interfaceType'), val: ['USB3.1 Type-C/A'] },
+          { name: $t('mall.spec.speed'), val: ['读速可达520MB/s以上,写速可达420MB/s'] },
+          { name: $t('mall.spec.operatingSystem'), val: ['支持Window7/8/10,MAC10.6以上'] },
         ]
         ]
       }
       }
     },
     },
@@ -358,112 +171,30 @@ export const partsData = {
     detail: [`${$t('mall.battery')}(${$t('mall.black')})`],
     detail: [`${$t('mall.battery')}(${$t('mall.black')})`],
     price: battery,
     price: battery,
     skuSn: 'U15604132404280703',
     skuSn: 'U15604132404280703',
-
     browdata: [
     browdata: [
-      {
-        small: 'battery-0',
-        big: 'battery-0',
-        // video: true
-      }, {
-        small: 'battery-1',
-        big: 'battery-1'
-      },
-      {
-        small: 'battery-2',
-        big: 'battery-2'
-      },
-      {
-        small: 'battery-3',
-        big: 'battery-3'
-      }],
+      { small: 'battery-0', big: 'battery-0' },
+      { small: 'battery-1', big: 'battery-1' },
+      { small: 'battery-2', big: 'battery-2' },
+      { small: 'battery-3', big: 'battery-3' },
+    ],
     shopInfo: {
     shopInfo: {
-      name: "四维深时专用电池",
+      name: $t('mall.battery'),
       price: 1999,
       price: 1999,
-      color: '黑色',
-      colorList: [
-        { name: '黑色', value: '黑色' },
-      ],
+      color: 'black',
+      colorList: [{ name: $t('mall.black'), value: 'black' }],
       showNumber: true,
       showNumber: true,
       partslist: [
       partslist: [
-        '服务:四维深时专用',
-        '容量:9700 mAh',
-        '接口:Type -C'
+        $t('mall.batteryParts.service'),
+        $t('mall.batteryParts.capacity'),
+        $t('mall.batteryParts.interface'),
       ]
       ]
     },
     },
-
     goodInfo: {
     goodInfo: {
-      name: "技术规格",
+      name: $t('mall.spec.title'),
       techSpecs: {
       techSpecs: {
-        name: '技术规格',
-        dec: '*由于受环境光、显示器色差、闪光等拍摄的影响,产品颜色上可能存在一定的彩色偏差,请见谅。',
-        list: [
-          {
-            name: '产品型号',
-            val: ['ET1004三脚架+Y-10云台']
-          },
-          {
-            name: '产品名称',
-            val: ['Y-10系列铝合金可反折扣式套餐']
-          },
-          {
-            name: '产品重量',
-            val: ['1.59KG(云台重量0.37KG)']
-          },
-          {
-            name: '云台类型',
-            val: [
-              'Y-10专业球形云台'
-            ]
-          },
-          {
-            name: '脚架材质',
-            val: [
-              '铝合金脚管+铝合金锻造主体'
-            ]
-          },
-          {
-            name: '脚管管径',
-            val: [
-              '粗25mm、细16mm'
-            ]
-          },
-          {
-            name: '脚管节数',
-            val: [
-              '4节'
-            ]
-          },
-          {
-            name: '安全负重',
-            val: [
-              '8KG'
-            ]
-          },
-          {
-            name: '收纳高度',
-            val: [
-              '56cm'
-            ]
-          },
-          {
-            name: '低角度',
-            val: [
-              '21.5cm'
-            ]
-          },
-          {
-            name: '展开高度',
-            val: [
-              '不升中轴高123.5cm、升中高147cm、升中轴+短中轴高度156.5cm(含云台、云台高度8.8cm)'
-            ]
-          },
-          {
-            name: '原厂标配',
-            val: [
-              '包装盒、脚架包、脚架套装、短中轴、脚架包背带、4只六角扳手、合格证、说明书'
-            ]
-          }
-        ]
+        name: $t('mall.spec.title'),
+        dec: $t('mall.spec.colorNote'),
+        list: tripodMainSpecs().list,
       }
       }
     },
     },
   },
   },
@@ -491,22 +222,16 @@ export const filterServiceMenuList = (menuList, userSerciceList) => {
 export const serciceData = {
 export const serciceData = {
   2: {
   2: {
     type: goodsType.server,
     type: goodsType.server,
-    name: '离线包下载服务',
+    name: $t('mall.offlinePackage.name'),
     icon: 'icon-store_benefit_download',
     icon: 'icon-store_benefit_download',
     price: 99,
     price: 99,
     skuSn: 'U15609161635760015',
     skuSn: 'U15609161635760015',
-    browdata: [
-      {
-
-        small: "download",
-        big: "download",
-      }],
-
+    browdata: [{ small: "download", big: "download" }],
     shopInfo: {
     shopInfo: {
-      name: "离线包下载服务",
+      name: $t('mall.offlinePackage.name'),
       price: 99,
       price: 99,
-      okText: '立即购买',
-      introduction: 'VR 场景离线包支持本地离线打开,无需安装应用,浏览器直接访问。',
+      okText: $t('mall.buyNow'),
+      introduction: $t('mall.offlinePackage.intro'),
       payType: 'download',
       payType: 'download',
       dateType: 6,
       dateType: 6,
     },
     },
@@ -514,56 +239,37 @@ export const serciceData = {
   1: {
   1: {
     type: goodsType.server,
     type: goodsType.server,
     icon: 'icon-store_benefit_panorama',
     icon: 'icon-store_benefit_panorama',
-    btnText: '限时免费',
-    name: '四维全景',
+    btnText: $t('mall.costType.0'),
+    name: $t('mall.panorama.name'),
     price: 99,
     price: 99,
     skuSn: 'U15609161635760015',
     skuSn: 'U15609161635760015',
-    browdata: [
-      {
-
-        small: "swqj",
-        big: "swqj",
-      }],
-
+    browdata: [{ small: "swqj", big: "swqj" }],
     shopInfo: {
     shopInfo: {
-      name: "四维全景",
+      name: $t('mall.panorama.name'),
       price: 99,
       price: 99,
-      okText: '立即购买',
-      introduction: '小程序需完成与四维服务的对接,才能解锁 VR 全景展示能力,享受完整流畅的 VR 体验。',
+      okText: $t('mall.costType.0'),
+      introduction: $t('mall.panorama.intro'),
       payType: 'service',
       payType: 'service',
     },
     },
   },
   },
   3: {
   3: {
     type: goodsType.server,
     type: goodsType.server,
-    name: 'AI训练',
+    name: $t('mall.aiTraining.name'),
     icon: 'icon-store_benefit_ai',
     icon: 'icon-store_benefit_ai',
     price: 0,
     price: 0,
     skuSn: 'U15609161635760015',
     skuSn: 'U15609161635760015',
-    browdata: [
-      {
-
-        small: "aiTraining",
-        big: "aiTraining",
-      }],
-
+    browdata: [{ small: "aiTraining", big: "aiTraining" }],
     shopInfo: {
     shopInfo: {
-      name: "AI训练",
+      name: $t('mall.aiTraining.name'),
       price: 199,
       price: 199,
-      okText: '联系客服',
-      introduction: '基于专业大模型技术,为客户提供专属 AI 模型训练服务。',
+      okText: $t('mall.costType.1'),
+      introduction: $t('mall.aiTraining.intro'),
       payType: 'contact',
       payType: 'contact',
     },
     },
   }
   }
 }
 }
 export const stepList = ref([
 export const stepList = ref([
-  {
-    id: 1,
-    name: "加入购物车",
-  }, {
-    id: 2,
-    name: "确认订单",
-  }, {
-    id: 3,
-    name: "下单",
-  },
-]);
+  { id: 1, name: $t('mall.addBuy') },
+  { id: 2, name: $t('mall.confirmOrder') },
+  { id: 3, name: $t('mall.submitOrder') },
+]);

+ 5 - 1
src/views/mall/details.vue

@@ -124,13 +124,17 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, reactive } from "vue";
+import { ref, reactive, computed } from "vue";
 import browse from "./components/browse.vue";
 import browse from "./components/browse.vue";
 import shopInfo from "./components/shopInfo.vue";
 import shopInfo from "./components/shopInfo.vue";
 import  { partsData } from './data.ts'
 import  { partsData } from './data.ts'
 import { useRoute } from 'vue-router'
 import { useRoute } from 'vue-router'
+import { useI18n } from 'vue-i18n'
 import { useCartStore } from "@/stores";
 import { useCartStore } from "@/stores";
 
 
+const { locale } = useI18n();
+const language = computed(() => locale.value);
+
 // route 是响应式路由对象
 // route 是响应式路由对象
 const route = useRoute()
 const route = useRoute()
 const cart = useCartStore();
 const cart = useCartStore();

+ 14 - 33
src/views/mall/goods.vue

@@ -1,13 +1,13 @@
 <template>
 <template>
   <div class="shop-page">
   <div class="shop-page">
     <div class="routerInfo container" style="margin: 20px auto; font-size: 16px;">
     <div class="routerInfo container" style="margin: 20px auto; font-size: 16px;">
-      <span class="cursor-pointer" style="color: #86909C" @click="$router.go(-1)">相机配件  /</span> <span style="colot: #202020">三脚架套装</span>
-
+      <span class="cursor-pointer" style="color: #86909C" @click="$router.go(-1)">{{ t('mall.typeList.0') }}  /</span>
+      <span style="colot: #202020">{{ goodInfoData.name }}</span>
     </div>
     </div>
     <div class="shop-info">
     <div class="shop-info">
       <div class="container flex items-center justify-around" style="margin-bottom: 40px">
       <div class="container flex items-center justify-around" style="margin-bottom: 40px">
         <browse
         <browse
-          :idata="[]"
+          :idata="browdata"
           :iactive="browdata[0]"
           :iactive="browdata[0]"
           :floder="'zhijiabrowse'"
           :floder="'zhijiabrowse'"
           class="product-img"
           class="product-img"
@@ -20,37 +20,18 @@
 
 
 <script setup>
 <script setup>
 import { ref, reactive } from "vue";
 import { ref, reactive } from "vue";
+import { useRoute } from "vue-router";
+import { useI18n } from "vue-i18n";
 import browse from "./components/browse.vue";
 import browse from "./components/browse.vue";
 import shopInfo from "./components/shopInfo.vue";
 import shopInfo from "./components/shopInfo.vue";
-// 相机配件数据源(可根据实际需求扩展)
-const browdata = ref([
-  {
-    small: "small-0",
-    big: "big-0",
-    video: true,
-  },
-  {
-    small: "small-1",
-    big: "big-1",
-  },
-  {
-    small: "small-2",
-    big: "big-2",
-  },
-  // {
-  //   small: 'small-3',
-  //   big: 'big-3'
-  // },
-  {
-    small: "small-4",
-    big: "big-4",
-  },
-]);
-const shopInfoData = reactive({
-  name: "三脚架套装",
-  price: 199,
-  introduction: 'VR 场景离线包支持本地离线打开,无需安装应用,浏览器直接访问。'
-  });
+import { partsData } from "./data.ts";
+
+const { t } = useI18n();
+const route = useRoute();
+const id = String(route.params.id || "7");
+const goodInfoData = partsData[id] || partsData[7];
+const browdata = ref(goodInfoData.browdata || []);
+const shopInfoData = reactive({ ...goodInfoData.shopInfo });
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>
@@ -128,4 +109,4 @@ const shopInfoData = reactive({
   cursor: pointer;
   cursor: pointer;
   font-size: 14px;
   font-size: 14px;
 }
 }
-</style>
+</style>

+ 7 - 7
src/views/mall/home.vue

@@ -3,20 +3,18 @@
     <div class="mall-page">
     <div class="mall-page">
       <!-- 页面头部标题 -->
       <!-- 页面头部标题 -->
       <div class="page-header">
       <div class="page-header">
-        <div class="main-title">四维时代商城</div>
-        <p class="sub-desc">
-          充分整合硬件、软件、增值服务,打造四维空间智能生态
-        </p>
+        <div class="main-title">{{ t('mall.homeTitle') }}</div>
+        <p class="sub-desc">{{ t('mall.homeSubtitle') }}</p>
       </div>
       </div>
       <!-- Tab 切换栏 -->
       <!-- Tab 切换栏 -->
       <a-tabs v-model:activeKey="activeTab" centered class="page-tabs">
       <a-tabs v-model:activeKey="activeTab" centered class="page-tabs">
-        <a-tab-pane key="member" tab="销售会员" >
+        <a-tab-pane key="member" :tab="t('mall.tabMember')">
             <ShopMemberCard />
             <ShopMemberCard />
         </a-tab-pane>
         </a-tab-pane>
-        <a-tab-pane key="parts" tab="耗材配件" >
+        <a-tab-pane key="parts" :tab="t('mall.tabParts')">
           <parts />
           <parts />
         </a-tab-pane>
         </a-tab-pane>
-        <a-tab-pane key="valueAdded" tab="增值服务" >
+        <a-tab-pane key="valueAdded" :tab="t('mall.tabValueAdded')">
           <valueAdded />
           <valueAdded />
         </a-tab-pane>
         </a-tab-pane>
       </a-tabs>
       </a-tabs>
@@ -29,9 +27,11 @@ import ShopMemberCard from "./components/shopMemberCard.vue";
 import parts from "./components/Parts.vue";
 import parts from "./components/Parts.vue";
 import valueAdded from "./components/valueAdded.vue";
 import valueAdded from "./components/valueAdded.vue";
 import { ref } from "vue";
 import { ref } from "vue";
+import { useI18n } from "vue-i18n";
 
 
 defineOptions({ name: "MallHome" });
 defineOptions({ name: "MallHome" });
 
 
+const { t } = useI18n();
 const activeTab = ref("member");
 const activeTab = ref("member");
 </script>
 </script>
 
 

+ 3 - 3
src/views/mall/membership.vue

@@ -55,8 +55,8 @@
           <span class="pay-logo stripe" aria-label="Stripe">stripe</span>
           <span class="pay-logo stripe" aria-label="Stripe">stripe</span>
         </template>
         </template>
         <template v-else>
         <template v-else>
-          <img src="@/assets/image/mall/ali-pay.png" alt="支付宝" class="pay-img" />
-          <img src="@/assets/image/mall/wechat-pay.png" alt="微信支付" class="pay-img" />
+          <img src="@/assets/image/mall/ali-pay.png" :alt="t('mall.aliPay')" class="pay-img" />
+          <img src="@/assets/image/mall/wechat-pay.png" :alt="t('mall.wechatPay')" class="pay-img" />
         </template>
         </template>
       </div>
       </div>
       <div class="footer-right">
       <div class="footer-right">
@@ -120,7 +120,7 @@ const apiIncrementTypeId = ref('')
 
 
 
 
 
 
-const memberTitle = computed(() => routeConfig.value.title)
+const memberTitle = computed(() => t(routeConfig.value.titleKey))
 
 
 const currencySymbol = computed(() => (isIntl ? '$' : '¥'))
 const currencySymbol = computed(() => (isIntl ? '$' : '¥'))
 
 

+ 3 - 3
src/views/mall/membershipConfig.ts

@@ -2,7 +2,7 @@ export type MemberType = 'freeMember' | 'member' | 'seMember'
 
 
 export interface MembershipRouteConfig {
 export interface MembershipRouteConfig {
   memberType: MemberType
   memberType: MemberType
-  title: string
+  titleKey: string
   termUnit: 'year' | 'month'
   termUnit: 'year' | 'month'
   /** 是否可选会员期限(未勾选自动续费时) */
   /** 是否可选会员期限(未勾选自动续费时) */
   selectableTerm: boolean
   selectableTerm: boolean
@@ -17,7 +17,7 @@ export const MEMBERSHIP_ROUTE_CONFIG: Record<string, MembershipRouteConfig> = {
   /** 专业会员 - 按年购买 */
   /** 专业会员 - 按年购买 */
   '5': {
   '5': {
     memberType: 'member',
     memberType: 'member',
-    title: '专业会员',
+    titleKey: 'member.Premium',
     termUnit: 'year',
     termUnit: 'year',
     selectableTerm: false,
     selectableTerm: false,
     maxTermMonths: 1,
     maxTermMonths: 1,
@@ -25,7 +25,7 @@ export const MEMBERSHIP_ROUTE_CONFIG: Record<string, MembershipRouteConfig> = {
   },
   },
   '7': {
   '7': {
     memberType: 'seMember',
     memberType: 'seMember',
-    title: '高级会员',
+    titleKey: 'member.Senior',
     termUnit: 'month',
     termUnit: 'month',
     selectableTerm: true,
     selectableTerm: true,
     maxTermMonths: 10,
     maxTermMonths: 10,

+ 20 - 4
src/views/mall/sercice.vue

@@ -59,6 +59,7 @@ import { useI18n } from "vue-i18n";
 import { Modal, message } from "ant-design-vue";
 import { Modal, message } from "ant-design-vue";
 import { ExclamationCircleFilled } from "@ant-design/icons-vue";
 import { ExclamationCircleFilled } from "@ant-design/icons-vue";
 import { useDownloadPay } from "@/hooks";
 import { useDownloadPay } from "@/hooks";
+import { serviceConfigList, userSerciceList } from '@/api/mall'
 
 
 const route = useRoute();
 const route = useRoute();
 const { t, locale } = useI18n();
 const { t, locale } = useI18n();
@@ -73,7 +74,6 @@ const specifications = ref(null);
 
 
 const displayShopInfo = computed(() => ({
 const displayShopInfo = computed(() => ({
   ...shopInfoData,
   ...shopInfoData,
-  price: isDownloadPay.value && unitPrice.value > 0 ? unitPrice.value : shopInfoData.price,
 }));
 }));
 
 
 const payType = computed(() => shopInfoData.payType || "download");
 const payType = computed(() => shopInfoData.payType || "download");
@@ -134,13 +134,17 @@ watch(paymentPending, (pending) => {
   closePaymentModal();
   closePaymentModal();
 });
 });
 
 
-const initPageData = () => {
+const initPageData = async () => {
   const data = goodInfoData.value;
   const data = goodInfoData.value;
   if (!data || !Object.keys(data).length) return;
   if (!data || !Object.keys(data).length) return;
   browdata.value = data.browdata || [];
   browdata.value = data.browdata || [];
-  Object.assign(shopInfoData, data.shopInfo || {});
+  let serviceList = await serviceConfigList()
+  let item = serviceList.find(ele => ele.id == serviceId.value)
+  Object.assign(shopInfoData, {...data.shopInfo, ...item, price: item.amount} || {});
+  console.log('shopInfoData', shopInfoData.value, item, data.shopInfo)
   goodInfo.value = data.goodInfo1 || null;
   goodInfo.value = data.goodInfo1 || null;
   specifications.value = data.goodInfo || null;
   specifications.value = data.goodInfo || null;
+
 };
 };
 
 
 const handleServiceBuy = async () => {
 const handleServiceBuy = async () => {
@@ -153,8 +157,20 @@ const handleServiceBuy = async () => {
 };
 };
 
 
 const handleBuy = async () => {
 const handleBuy = async () => {
+  const userServiceList = await userSerciceList();
+  console.log('userServiceList', userServiceList)
+  let item = userServiceList.find(ele => ele.id == serviceId.value)
+  if(!shopInfoData.isPublic){
+    let supportLink = 'https://affim.baidu.com/unique_54577246/chat?siteId=22911125&userId=54577246&siteToken=29903ef13e9612af77266f2b97d1c715'
+    window.open(supportLink, '_blank')
+    return;
+  }
+  if (item) {
+    message.info(t('mall.paySuccessYouHave'));
+    return;
+  }
   if (payType.value === "contact") {
   if (payType.value === "contact") {
-    message.info(t("mall.purchaseTpis"));
+    message.info(t('mall.purchaseTpis'));
     return;
     return;
   }
   }
   if (submitting.value) return;
   if (submitting.value) return;

+ 17 - 41
src/views/order/components/InvoicePreview.vue

@@ -1,65 +1,56 @@
 <template>
 <template>
   <div class="invoice-card-wrap">
   <div class="invoice-card-wrap">
     <div class="invoice-card">
     <div class="invoice-card">
-      <!-- 顶部票据锯齿撕口【修复版】 -->
       <div class="invoice-sawtooth">
       <div class="invoice-sawtooth">
-        <!-- 动态标题 -->
         <div class="invoice-title">
         <div class="invoice-title">
           <span class="title-text">{{ titleText }}</span>
           <span class="title-text">{{ titleText }}</span>
         </div>
         </div>
       </div>
       </div>
       <div class="invoice-content">
       <div class="invoice-content">
-
-        <!-- 公共通用字段 -->
         <div class="invoice-item">
         <div class="invoice-item">
-          <span class="label">发票类型:</span>
+          <span class="label">{{ t('invoicePreview.typeLabel') }}</span>
           <span class="value">{{ titleText }}</span>
           <span class="value">{{ titleText }}</span>
         </div>
         </div>
         <div class="invoice-item">
         <div class="invoice-item">
-          <span class="label">发票抬头:</span>
+          <span class="label">{{ t('invoicePreview.titleLabel') }}</span>
           <span class="value">{{ info.title }}</span>
           <span class="value">{{ info.title }}</span>
         </div>
         </div>
         <div class="invoice-item">
         <div class="invoice-item">
-          <span class="label">税务登记号:</span>
+          <span class="label">{{ t('invoicePreview.codeLabel') }}</span>
           <span class="value">{{ info.code }}</span>
           <span class="value">{{ info.code }}</span>
         </div>
         </div>
 
 
-        <!-- 电子普票独有:电子邮箱 -->
         <div v-if="invoiceType === 2" class="invoice-item">
         <div v-if="invoiceType === 2" class="invoice-item">
-          <span class="label">电子邮箱:</span>
+          <span class="label">{{ t('invoicePreview.emailLabel') }}</span>
           <span class="value">{{ info.emailAddress }}</span>
           <span class="value">{{ info.emailAddress }}</span>
         </div>
         </div>
 
 
-        <!-- 专票独有:银行注册信息 -->
         <template v-if="invoiceType == 3">
         <template v-if="invoiceType == 3">
           <div class="invoice-item">
           <div class="invoice-item">
-            <span class="label">注册地址:</span>
+            <span class="label">{{ t('invoicePreview.addressLabel') }}</span>
             <span class="value">{{ info.organizedAddress }}</span>
             <span class="value">{{ info.organizedAddress }}</span>
           </div>
           </div>
           <div class="invoice-item">
           <div class="invoice-item">
-            <span class="label">注册电话:</span>
+            <span class="label">{{ t('invoicePreview.phoneLabel') }}</span>
             <span class="value">{{ info.registerPhone }}</span>
             <span class="value">{{ info.registerPhone }}</span>
           </div>
           </div>
           <div class="invoice-item">
           <div class="invoice-item">
-            <span class="label">开户银行:</span>
+            <span class="label">{{ t('invoicePreview.bankLabel') }}</span>
             <span class="value">{{ info.bankName }}</span>
             <span class="value">{{ info.bankName }}</span>
           </div>
           </div>
           <div class="invoice-item">
           <div class="invoice-item">
-            <span class="label">银行账号:</span>
+            <span class="label">{{ t('invoicePreview.accountLabel') }}</span>
             <span class="value">{{ info.bankAccount }}</span>
             <span class="value">{{ info.bankAccount }}</span>
           </div>
           </div>
         </template>
         </template>
 
 
-        <!-- 公共:联系电话 -->
         <div class="invoice-item">
         <div class="invoice-item">
-          <span class="label">联系电话:</span>
+          <span class="label">{{ t('invoicePreview.contactLabel') }}</span>
           <span class="value">{{ info.contactPhone }}</span>
           <span class="value">{{ info.contactPhone }}</span>
         </div>
         </div>
 
 
-        <!-- 分割虚线 -->
         <div class="divider-line"></div>
         <div class="divider-line"></div>
 
 
-        <!-- 底部动态备注提示 -->
         <div class="invoice-tip">
         <div class="invoice-tip">
           {{ tipText }}
           {{ tipText }}
         </div>
         </div>
@@ -70,8 +61,8 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { computed } from 'vue';
 import { computed } from 'vue';
+import { useI18n } from 'vue-i18n';
 
 
-// 发票类型枚举
 type InvoiceType = 2 | 'special';
 type InvoiceType = 2 | 'special';
 
 
 interface InvoiceInfo {
 interface InvoiceInfo {
@@ -80,14 +71,11 @@ interface InvoiceInfo {
   invoiceTypeName: string;
   invoiceTypeName: string;
   invoiceTitle: string;
   invoiceTitle: string;
   taxNo: string;
   taxNo: string;
-  // 电子普票字段
   emailAddress?: string;
   emailAddress?: string;
-  // 专票专属字段
   organizedAddress?: string;
   organizedAddress?: string;
   registerPhone?: string;
   registerPhone?: string;
   bankName?: string;
   bankName?: string;
   bankAccount?: string;
   bankAccount?: string;
-  // 通用
   contactPhone: string;
   contactPhone: string;
 }
 }
 
 
@@ -95,21 +83,20 @@ const props = defineProps<{
   info: InvoiceInfo;
   info: InvoiceInfo;
 }>();
 }>();
 
 
-// 当前发票类型
+const { t } = useI18n();
+
 const invoiceType = computed(() => props.info.type);
 const invoiceType = computed(() => props.info.type);
 
 
-// 动态标题文字
 const titleText = computed(() => {
 const titleText = computed(() => {
   return invoiceType.value === 2
   return invoiceType.value === 2
-    ? '增值税普票电子发票'
-    : '增值税专用发票';
+    ? t('invoicePreview.electronicTitle')
+    : t('invoicePreview.specialTitle');
 });
 });
 
 
-// 底部动态提示文案
 const tipText = computed(() => {
 const tipText = computed(() => {
   return invoiceType.value === 2
   return invoiceType.value === 2
-    ? '注:电子发票将于每个月 15 日开具内发送至您的邮箱!'
-    : '注:专票将于每个月 15 日开具并邮寄到您的默认收货地址,请注意查收!';
+    ? t('mall.myOrders.typeStip.1')
+    : t('mall.myOrders.typeStip.0');
 });
 });
 </script>
 </script>
 
 
@@ -122,20 +109,14 @@ const tipText = computed(() => {
 .invoice-card {
 .invoice-card {
   width: 100%;
   width: 100%;
   max-width: 640px;
   max-width: 640px;
-
   border-radius: 10px;
   border-radius: 10px;
-  /* 关键:必须开启裁切,锯齿才不会溢出 */
   overflow: hidden;
   overflow: hidden;
 }
 }
 
 
-// ========== 修复后锯齿样式 ==========
 .invoice-sawtooth {
 .invoice-sawtooth {
   width: 100%;
   width: 100%;
   height: 100px;
   height: 100px;
-  
   background-image: url(@/assets/image/order/infoBg.png);
   background-image: url(@/assets/image/order/infoBg.png);
-  /* 三角锯齿核心渐变,格子宽度24px、高度24px,和容器等高 */
-
   background-size: 100%;
   background-size: 100%;
   background-repeat: no-repeat;
   background-repeat: no-repeat;
 }
 }
@@ -144,7 +125,6 @@ const tipText = computed(() => {
   padding: 20px 30px 30px;
   padding: 20px 30px 30px;
   background-color: #ffffff;
   background-color: #ffffff;
 }
 }
-// 红色椭圆标题
 .invoice-title {
 .invoice-title {
   text-align: center;
   text-align: center;
   padding-top: 60px;
   padding-top: 60px;
@@ -157,7 +137,6 @@ const tipText = computed(() => {
     border-radius: 999px;
     border-radius: 999px;
   }
   }
 }
 }
-// 单行信息左右排版
 .invoice-item {
 .invoice-item {
   display: flex;
   display: flex;
   margin-bottom: 20px;
   margin-bottom: 20px;
@@ -178,17 +157,14 @@ const tipText = computed(() => {
     word-break: break-all;
     word-break: break-all;
   }
   }
 }
 }
-// 底部分割虚线
 .divider-line {
 .divider-line {
   height: 1px;
   height: 1px;
   background-image: linear-gradient(to right, #ddd 50%, transparent 50%);
   background-image: linear-gradient(to right, #ddd 50%, transparent 50%);
   background-size: 8px 1px;
   background-size: 8px 1px;
   margin: 20px 0 20px;
   margin: 20px 0 20px;
 }
 }
-// 底部红色提示栏
 .invoice-tip {
 .invoice-tip {
   background: #fff0f0;
   background: #fff0f0;
-  color: #dd2222;
   padding: 6px;
   padding: 6px;
   width: 100%;
   width: 100%;
   border-radius: 8px;
   border-radius: 8px;
@@ -197,4 +173,4 @@ const tipText = computed(() => {
   font-size: 12px;
   font-size: 12px;
   color: #F53F3F;
   color: #F53F3F;
 }
 }
-</style>
+</style>

+ 48 - 76
src/views/order/components/invoiceModal.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
   <a-modal
   <a-modal
-    title="开具发票"
+    :title="t('mall.myOrders.invoice')"
     :open="visible"
     :open="visible"
     @cancel="handleCancel"
     @cancel="handleCancel"
     :width="600"
     :width="600"
@@ -13,83 +13,74 @@
       :label-col="{ style: { width: '120px' } }"
       :label-col="{ style: { width: '120px' } }"
       :wrapper-col="{ span: 18 }"
       :wrapper-col="{ span: 18 }"
     >
     >
-      <!-- 发票类型下拉框 -->
-      <a-form-item label="发票类型" name="invoiceType">
+      <a-form-item :label="t('mall.account.invoiceModule.invoiceType')" name="invoiceType">
         <a-select
         <a-select
           size="large"
           size="large"
           v-model:value="formData.invoiceType"
           v-model:value="formData.invoiceType"
           @change="handleTypeChange"
           @change="handleTypeChange"
         >
         >
-          <a-select-option :value="2"
-            >增值税普票(电子发票)</a-select-option
-          >
-          <a-select-option :value="3">增值税专用发票</a-select-option>
+          <a-select-option :value="2">{{ t('invoiceForm.electronicOption') }}</a-select-option>
+          <a-select-option :value="3">{{ t('mall.myOrders.type3') }}</a-select-option>
         </a-select>
         </a-select>
       </a-form-item>
       </a-form-item>
 
 
-      <!-- 公共必填项:发票抬头 -->
-      <a-form-item label="发票抬头" name="title">
+      <a-form-item :label="t('mall.myOrders.title')" name="title">
         <a-input size="large"
         <a-input size="large"
           v-model:value="formData.title"
           v-model:value="formData.title"
-          placeholder="请输入发票抬头"
+          :placeholder="t('mall.account.placeholder.title')"
         />
         />
       </a-form-item>
       </a-form-item>
 
 
-      <!-- 公共必填项:税务登记号 -->
-      <a-form-item label="税务登记号" name="code">
+      <a-form-item :label="t('mall.account.invoiceModule.code')" name="code">
         <a-input size="large"
         <a-input size="large"
           v-model:value="formData.code"
           v-model:value="formData.code"
-          placeholder="请输入18位税务登记号"
+          :placeholder="t('mall.account.placeholder.code')"
           maxlength="18"
           maxlength="18"
         />
         />
       </a-form-item>
       </a-form-item>
 
 
-      <!-- 电子发票专属:电子邮箱 -->
       <a-form-item 
       <a-form-item 
         v-if="formData.invoiceType == 2"
         v-if="formData.invoiceType == 2"
-        label="电子邮箱"
+        :label="t('mall.myOrders.email')"
         name="emailAddress"
         name="emailAddress"
       >
       >
-        <a-input size="large" v-model:value="formData.emailAddress" placeholder="请输入电子邮箱" />
+        <a-input size="large" v-model:value="formData.emailAddress" :placeholder="t('invoiceForm.inputEmail')" />
       </a-form-item>
       </a-form-item>
 
 
-      <!-- 专票专属字段 start -->
       <template v-if="formData.invoiceType == 3">
       <template v-if="formData.invoiceType == 3">
-        <a-form-item label="注册地址" name="organizedAddress">
+        <a-form-item :label="t('mall.myOrders.organizedAddress')" name="organizedAddress">
           <a-input size="large"
           <a-input size="large"
             v-model:value="formData.organizedAddress"
             v-model:value="formData.organizedAddress"
-            placeholder="请输入注册地址"
+            :placeholder="t('invoiceForm.inputAddress')"
           />
           />
         </a-form-item>
         </a-form-item>
 
 
-        <a-form-item label="注册电话" name="registerPhone">
+        <a-form-item :label="t('mall.myOrders.registerPhone')" name="registerPhone">
           <a-input size="large"
           <a-input size="large"
             v-model:value="formData.registerPhone"
             v-model:value="formData.registerPhone"
-            placeholder="请输入注册电话"
+            :placeholder="t('invoiceForm.inputRegisterPhone')"
           />
           />
         </a-form-item>
         </a-form-item>
 
 
-        <a-form-item label="开户银行" name="bankName">
+        <a-form-item :label="t('mall.myOrders.bankName')" name="bankName">
           <a-input size="large"
           <a-input size="large"
             v-model:value="formData.bankName"
             v-model:value="formData.bankName"
-            placeholder="请输入开户银行"
+            :placeholder="t('invoiceForm.inputBank')"
           />
           />
         </a-form-item>
         </a-form-item>
 
 
-        <a-form-item label="银行账号" name="bankAccount">
+        <a-form-item :label="t('mall.myOrders.bankAccount')" name="bankAccount">
           <a-input size="large"
           <a-input size="large"
             v-model:value="formData.bankAccount"
             v-model:value="formData.bankAccount"
-            placeholder="请输入银行账号"
+            :placeholder="t('invoiceForm.inputBankAccount')"
           />
           />
         </a-form-item>
         </a-form-item>
       </template>
       </template>
-      <!-- 专票专属字段 end -->
 
 
-      <!-- 公共项:联系电话 -->
-      <a-form-item label="联系电话" name="shipMobile">
+      <a-form-item :label="t('invoiceForm.contactPhone')" name="shipMobile">
         <a-input size="large"
         <a-input size="large"
           v-model:value="formData.shipMobile"
           v-model:value="formData.shipMobile"
-          placeholder="请输入联系电话"
+          :placeholder="t('invoiceForm.inputContactPhone')"
         />
         />
       </a-form-item>
       </a-form-item>
     </a-form>
     </a-form>
@@ -103,16 +94,17 @@ import {
   defineEmits,
   defineEmits,
   defineProps,
   defineProps,
   watch,
   watch,
-  onUnmounted,
+  computed,
 } from "vue";
 } from "vue";
-import { debounce } from "lodash-es";
+import { useI18n } from "vue-i18n";
 import { invoiceOpen } from "@/api/equity";
 import { invoiceOpen } from "@/api/equity";
-// 1. 定义Props(增加类型校验和注释)
+
+const { t } = useI18n();
+
 const props = defineProps({
 const props = defineProps({
   visible: {
   visible: {
     type: Boolean,
     type: Boolean,
     default: false,
     default: false,
-    description: "弹窗是否可见",
   },
   },
   item: {
   item: {
     type: Object,
     type: Object,
@@ -120,18 +112,11 @@ const props = defineProps({
   }
   }
 });
 });
 
 
-// 2. 定义Emits(明确事件类型)
-const emit = defineEmits(["update:visible", "confirm"], {
-  confirm: (value) => {
-    // 校验确认参数:如果是重命名则校验字符串,选择用户则校验数组
-    return typeof value === "string" || Array.isArray(value);
-  },
-});
-// 表单实例
+const emit = defineEmits(["update:visible", "confirm"]);
+
 const formRef = ref();
 const formRef = ref();
 const submitLoading = ref(false);
 const submitLoading = ref(false);
 
 
-// 初始化表单
 const formData = reactive({
 const formData = reactive({
   invoiceType: 2,
   invoiceType: 2,
   title: '',
   title: '',
@@ -144,59 +129,50 @@ const formData = reactive({
   shipMobile: '',
   shipMobile: '',
 });
 });
 
 
-// 动态校验规则
-const formRules = reactive({
-  // 通用校验
-  invoiceType: [{ required: true, message: '请选择发票类型', trigger: 'change' }],
-  title: [{ required: true, message: '请输入发票抬头', trigger: 'blur' }],
+const formRules = computed(() => ({
+  invoiceType: [{ required: true, message: t('invoiceForm.selectType'), trigger: 'change' }],
+  title: [{ required: true, message: t('invoiceForm.inputTitle'), trigger: 'blur' }],
   code: [
   code: [
-    { required: true, message: '请输入税务登记号', trigger: 'blur' },
-    { pattern: /^[0-9A-Z]{18}$/, message: '税务登记号必须18位', trigger: 'blur' },
+    { required: true, message: t('invoiceForm.inputCode'), trigger: 'blur' },
+    { pattern: /^[0-9A-Z]{18}$/, message: t('invoiceForm.codeLength'), trigger: 'blur' },
   ],
   ],
   shipMobile: [
   shipMobile: [
-    { required: true, message: '请输入联系电话', trigger: 'blur' },
-    { pattern: /^1[3-9]\d{9}$/, message: '手机号格式错误', trigger: 'blur' },
+    { required: true, message: t('invoiceForm.inputContactPhone'), trigger: 'blur' },
+    { pattern: /^1[3-9]\d{9}$/, message: t('invoiceForm.phoneInvalid'), trigger: 'blur' },
   ],
   ],
-  // 电子票邮箱校验
   emailAddress: [
   emailAddress: [
-    { required: true, message: '请输入电子邮箱', trigger: 'blur' },
-    { pattern: /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, message: '邮箱格式错误', trigger: 'blur' },
+    { required: true, message: t('invoiceForm.inputEmail'), trigger: 'blur' },
+    { pattern: /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, message: t('invoiceForm.emailInvalid'), trigger: 'blur' },
   ],
   ],
-  // 专票专属校验
-  organizedAddress: [{ required: true, message: '请输入注册地址', trigger: 'blur' }],
+  organizedAddress: [{ required: true, message: t('invoiceForm.inputAddress'), trigger: 'blur' }],
   registerPhone: [
   registerPhone: [
-    { required: true, message: '请输入注册电话', trigger: 'blur' },
-    { pattern: /^1[3-9]\d{9}$/, message: '手机号格式错误', trigger: 'blur' },
+    { required: true, message: t('invoiceForm.inputRegisterPhone'), trigger: 'blur' },
+    { pattern: /^1[3-9]\d{9}$/, message: t('invoiceForm.phoneInvalid'), trigger: 'blur' },
   ],
   ],
-  bankName: [{ required: true, message: '请输入开户银行', trigger: 'blur' }],
+  bankName: [{ required: true, message: t('invoiceForm.inputBank'), trigger: 'blur' }],
   bankAccount: [
   bankAccount: [
-    { required: true, message: '请输入银行账号', trigger: 'blur' },
-    { pattern: /^\d{16,19}$/, message: '银行账号格式错误', trigger: 'blur' },
+    { required: true, message: t('invoiceForm.inputBankAccount'), trigger: 'blur' },
+    { pattern: /^\d{16,19}$/, message: t('invoiceForm.bankAccountInvalid'), trigger: 'blur' },
   ],
   ],
-});
+}));
 
 
-// 切换发票类型,清空多余字段、清除校验提示
 const handleTypeChange = () => {
 const handleTypeChange = () => {
-  // 切换时清空所有表单校验报错
   formRef.value?.clearValidate();
   formRef.value?.clearValidate();
   if (formData.invoiceType == 2) {
   if (formData.invoiceType == 2) {
-    // 切换电子票,清空专票字段
     formData.organizedAddress = '';
     formData.organizedAddress = '';
     formData.registerPhone = '';
     formData.registerPhone = '';
     formData.bankName = '';
     formData.bankName = '';
     formData.bankAccount = '';
     formData.bankAccount = '';
   } else {
   } else {
-    // 切换专票,清空电子票邮箱
     formData.emailAddress = '';
     formData.emailAddress = '';
   }
   }
 };
 };
 
 
-// 弹窗关闭监听,重置表单
-watch(props.visible, (newVal) => {
+watch(() => props.visible, (newVal) => {
   if (!newVal) {
   if (!newVal) {
     formRef.value?.resetFields();
     formRef.value?.resetFields();
     Object.assign(formData, {
     Object.assign(formData, {
-      invoiceType: '1',
+      invoiceType: 2,
       title: '',
       title: '',
       code: '',
       code: '',
       emailAddress: '',
       emailAddress: '',
@@ -212,19 +188,16 @@ watch(props.visible, (newVal) => {
 const handleCancel = () => {
 const handleCancel = () => {
   emit("update:visible", false);
   emit("update:visible", false);
 };
 };
-// 提交授权
+
 const handleSubmit = async () => {
 const handleSubmit = async () => {
   try {
   try {
     submitLoading.value = true;
     submitLoading.value = true;
-    // 表单校验
     const params = await formRef.value?.validate();
     const params = await formRef.value?.validate();
     await invoiceOpen({...params, orderId: props.item.id, type: params.invoiceType, consumeType: props.item.orderType === 1 ? 0 : props.item.orderType == 0 ? 2 : 3 })
     await invoiceOpen({...params, orderId: props.item.id, type: params.invoiceType, consumeType: props.item.orderType === 1 ? 0 : props.item.orderType == 0 ? 2 : 3 })
-    // 抛出表单数据给父组件
     emit('confirm', { ...formData });
     emit('confirm', { ...formData });
-    // 关闭弹窗
     emit('update:visible', false);
     emit('update:visible', false);
   } catch (err) {
   } catch (err) {
-    console.log('表单校验失败', err);
+    console.log('form validate failed', err);
   } finally {
   } finally {
     submitLoading.value = false;
     submitLoading.value = false;
   }
   }
@@ -232,10 +205,9 @@ const handleSubmit = async () => {
 </script>
 </script>
 
 
 <style scoped lang="less">
 <style scoped lang="less">
-// 仅保留实际使用的样式,删除无用样式
 :deep(.ant-modal-footer) {
 :deep(.ant-modal-footer) {
   display: flex;
   display: flex;
   justify-content: flex-end;
   justify-content: flex-end;
   gap: 12px;
   gap: 12px;
 }
 }
-</style>
+</style>

+ 25 - 37
src/views/order/details.vue

@@ -1,33 +1,32 @@
 <template>
 <template>
   <div class="project-detail-container">
   <div class="project-detail-container">
-    <!-- 标题 -->
-    <h2 class="project-title">订单编号:{{ item.orderSn }}</h2>
+    <h2 class="project-title">{{ t('orderAdmin.orderDetail.orderSnTitle', { sn: item.orderSn }) }}</h2>
     <div class="divider"></div>
     <div class="divider"></div>
 
 
     <div class="content-wrapper">
     <div class="content-wrapper">
       <div class="right-section">
       <div class="right-section">
-        <h3 class="section-title">订单信息</h3>
+        <h3 class="section-title">{{ t('orderAdmin.orderDetail.orderInfo') }}</h3>
         <div class="info-list">
         <div class="info-list">
           <div class="info-item">
           <div class="info-item">
-            <span class="label">类型:</span>
+            <span class="label">{{ t('orderAdmin.orderDetail.type') }}</span>
             <span class="value">{{
             <span class="value">{{
               getOrderType.find((ele) => ele.value == item.orderType)?.label
               getOrderType.find((ele) => ele.value == item.orderType)?.label
             }}</span>
             }}</span>
           </div>
           </div>
           <div class="info-item">
           <div class="info-item">
-            <span class="label">订单编号:</span>
+            <span class="label">{{ t('orderAdmin.orderDetail.orderSn') }}</span>
             <span class="value">{{ item.orderSn }}</span>
             <span class="value">{{ item.orderSn }}</span>
           </div>
           </div>
           <div class="info-item">
           <div class="info-item">
-            <span class="label">下单时间:</span>
+            <span class="label">{{ t('orderAdmin.orderDetail.createTime') }}</span>
             <span class="value">{{ item.createTime }}</span>
             <span class="value">{{ item.createTime }}</span>
           </div>
           </div>
           <div class="info-item">
           <div class="info-item">
-            <span class="label">付款时间:</span>
+            <span class="label">{{ t('orderAdmin.orderDetail.payTime') }}</span>
             <span class="value">{{ item.tradeTime }}</span>
             <span class="value">{{ item.tradeTime }}</span>
           </div>
           </div>
           <div class="info-item">
           <div class="info-item">
-            <span class="label">内容:</span>
+            <span class="label">{{ t('orderAdmin.orderDetail.content') }}</span>
             <span class="value">
             <span class="value">
               <span v-if="item.orderType == 0">{{$t(item.memberLevels === 'SE' ? 'member.Senior' : 'member.Premium')}}(4DKanKan)</span>
               <span v-if="item.orderType == 0">{{$t(item.memberLevels === 'SE' ? 'member.Senior' : 'member.Premium')}}(4DKanKan)</span>
               <span v-else-if="item.orderType == 1">
               <span v-else-if="item.orderType == 1">
@@ -41,27 +40,27 @@
             </span>
             </span>
           </div>
           </div>
           <div class="info-item">
           <div class="info-item">
-            <span class="label">开通时间:</span>
+            <span class="label">{{ t('orderAdmin.orderDetail.startTime') }}</span>
             <span class="value">-</span>
             <span class="value">-</span>
           </div>
           </div>
           <div class="info-item">
           <div class="info-item">
-            <span class="label">到期时间:</span>
+            <span class="label">{{ t('orderAdmin.orderDetail.expireTime') }}</span>
             <span class="value">2024-08-27 21:12</span>
             <span class="value">2024-08-27 21:12</span>
           </div>
           </div>
           <div class="info-item">
           <div class="info-item">
-            <span class="label">总价:</span>
+            <span class="label">{{ t('orderAdmin.orderDetail.totalPrice') }}</span>
             <span class="value">{{
             <span class="value">{{
               item.amount || item.price || item.totalAmount
               item.amount || item.price || item.totalAmount
             }}</span>
             }}</span>
           </div>
           </div>
           <div class="info-item">
           <div class="info-item">
-            <span class="label">支付方式:</span>
+            <span class="label">{{ t('orderAdmin.orderDetail.payType') }}</span>
             <span class="value">{{
             <span class="value">{{
               $t(`mall.payTypeStr.${item.payType}`)
               $t(`mall.payTypeStr.${item.payType}`)
             }}</span>
             }}</span>
           </div>
           </div>
           <div class="info-item">
           <div class="info-item">
-            <span class="label">订单状态:</span>
+            <span class="label">{{ t('orderAdmin.orderDetail.orderStatus') }}</span>
             <span class="value">
             <span class="value">
               <span v-if="getOrderStatusInfo(item).textKey" class="expreeNum">
               <span v-if="getOrderStatusInfo(item).textKey" class="expreeNum">
                 {{ $t(getOrderStatusInfo(item).textKey) }}
                 {{ $t(getOrderStatusInfo(item).textKey) }}
@@ -78,15 +77,12 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import {
-  FileProtectOutlined,
-  DownloadOutlined,
-  QuestionCircleOutlined,
-} from "@ant-design/icons-vue";
 import { ref, computed } from "vue";
 import { ref, computed } from "vue";
+import { useI18n } from "vue-i18n";
 import { getOrderType, getStatus, orderGoodsInfo } from "./data";
 import { getOrderType, getStatus, orderGoodsInfo } from "./data";
-import  { partsData } from '../mall/data'
 import { useOrderStore } from "@/stores";
 import { useOrderStore } from "@/stores";
+
+const { t } = useI18n();
 const orderStore = useOrderStore();
 const orderStore = useOrderStore();
 const item = computed(() => orderStore.getOrderItem);
 const item = computed(() => orderStore.getOrderItem);
 
 
@@ -106,7 +102,6 @@ const getOrderStatusInfo = (item) => {
   );
   );
 };
 };
 
 
-// 进度条百分比(可以和后端状态联动)
 const progressPercent = ref(10);
 const progressPercent = ref(10);
 </script>
 </script>
 
 
@@ -137,8 +132,6 @@ const progressPercent = ref(10);
     }
     }
 
 
     .right-section {
     .right-section {
-      // border-left: 1px solid #e5e6eb;
-      // padding-left: 40px;
       min-height: calc(100vh - 248px);
       min-height: calc(100vh - 248px);
     }
     }
 
 
@@ -151,12 +144,11 @@ const progressPercent = ref(10);
     }
     }
 
 
     .progress-wrap {
     .progress-wrap {
-      width: 100%; // 确保进度条占满父容器
+      width: 100%;
       display: flex;
       display: flex;
       align-items: center;
       align-items: center;
       gap: 6px;
       gap: 6px;
-      // margin-top: 10px; // 和产品名称保持统一间距
-      margin-bottom: 40px; // 和产品名称保持统一间距
+      margin-bottom: 40px;
       font-weight: normal;
       font-weight: normal;
       font-size: 12px;
       font-size: 12px;
       color: #4e5969;
       color: #4e5969;
@@ -166,7 +158,6 @@ const progressPercent = ref(10);
         margin-right: 23px;
         margin-right: 23px;
       }
       }
 
 
-      // 调整进度条样式,避免溢出
       :deep(.ant-progress) {
       :deep(.ant-progress) {
         width: 100%;
         width: 100%;
       }
       }
@@ -174,17 +165,15 @@ const progressPercent = ref(10);
         width: 100%;
         width: 100%;
       }
       }
     }
     }
-    // 单个附加产物项 - 核心调整:Flex 布局垂直对齐 + 宽度分配
     .product-item-top {
     .product-item-top {
       display: flex;
       display: flex;
       justify-content: space-between;
       justify-content: space-between;
-      align-items: flex-start; // 改为顶部对齐,避免按钮和进度条垂直错位
-      // margin-bottom: 24px;
+      align-items: flex-start;
       width: 100%;
       width: 100%;
 
 
       .product-info {
       .product-info {
-        flex: 1; // 占满除按钮外的所有宽度
-        padding-right: 16px; // 和按钮保持间距
+        flex: 1;
+        padding-right: 16px;
 
 
         .product-name {
         .product-name {
           font-weight: normal;
           font-weight: normal;
@@ -213,9 +202,8 @@ const progressPercent = ref(10);
       .action-buttons {
       .action-buttons {
         display: flex;
         display: flex;
         gap: 12px;
         gap: 12px;
-        flex-shrink: 0; // 按钮区域不收缩
-        align-items: flex-start; // 按钮顶部对齐
-        // margin-top: 10px; // 和左侧产品名称垂直对齐
+        flex-shrink: 0;
+        align-items: flex-start;
       }
       }
 
 
       .operation {
       .operation {
@@ -224,11 +212,11 @@ const progressPercent = ref(10);
         min-width: 50px;
         min-width: 50px;
         display: flex;
         display: flex;
         align-items: center;
         align-items: center;
-        justify-content: center; // 图标居中
+        justify-content: center;
 
 
         .anticon {
         .anticon {
           position: relative;
           position: relative;
-          top: 0; // 移除偏移,避免图标错位
+          top: 0;
         }
         }
       }
       }
     }
     }
@@ -251,4 +239,4 @@ const progressPercent = ref(10);
     }
     }
   }
   }
 }
 }
-</style>
+</style>

+ 3 - 3
src/views/order/list.vue

@@ -125,7 +125,7 @@ const hendleCooperation = () => {
     Modal.confirm({
     Modal.confirm({
       title: t("common.tip"),
       title: t("common.tip"),
       icon: createVNode(ExclamationCircleFilled),
       icon: createVNode(ExclamationCircleFilled),
-      content: "计算中/已封存的场景无法协作",
+      content: t("scene.sceneAdmin.cooperationBlockedTip"),
       centered: true,
       centered: true,
       // okText: '确认',
       // okText: '确认',
       // cancelText: '取消',
       // cancelText: '取消',
@@ -178,7 +178,7 @@ watch([()=>orderType.value, ()=>orderSn.value], () => {
 </script>
 </script>
 <template>
 <template>
   <div class="scene-layout">
   <div class="scene-layout">
-    <div class="routeName">订单管理</div>
+    <div class="routeName">{{ t('menu.orderManage') }}</div>
     <div class="table w-full">
     <div class="table w-full">
       <!-- :customRow="handleCustomRow" -->
       <!-- :customRow="handleCustomRow" -->
       <BaseTable
       <BaseTable
@@ -218,7 +218,7 @@ watch([()=>orderType.value, ()=>orderSn.value], () => {
                   style="width: 400px; padding: 0 11px"
                   style="width: 400px; padding: 0 11px"
                   size="large"
                   size="large"
                   allowClear
                   allowClear
-                  placeholder="请输入商品名称或订单编号"
+                  :placeholder="t('orderAdmin.searchPlaceholder')"
                 >
                 >
                   <template #prefix><SearchOutlined /></template>
                   <template #prefix><SearchOutlined /></template>
                 </a-input>
                 </a-input>

+ 22 - 48
src/views/scene/components/cooperation.vue

@@ -5,22 +5,7 @@
     :footer="null"
     :footer="null"
     :width="600"
     :width="600"
   >
   >
-    <!-- 搜索 + 添加按钮 -->
     <div class="modal-header">
     <div class="modal-header">
-      <!-- <a-select
-        v-model:value="value"
-        show-search
-        :placeholder="$t('common.search')"
-        style="height: 100%; flex: 1 1 0%"
-        :default-active-first-option="false"
-        :show-arrow="false"
-        :filter-option="false"
-        :not-found-content="null"
-        :options="data"
-        size="large"
-        @search="handleSearch"
-        @change="handleChange"
-      ></a-select> -->
       <a-input
       <a-input
         size="large"
         size="large"
         v-model:value="userName"
         v-model:value="userName"
@@ -32,11 +17,10 @@
         type="primary"
         type="primary"
         size="large"
         size="large"
         @click="handleAdd"
         @click="handleAdd"
-        >添加协作</a-button
+        >{{ t('scene.cooperationModal.addCooperation') }}</a-button
       >
       >
     </div>
     </div>
 
 
-    <!-- 表格 -->
     <a-table
     <a-table
       :columns="columns"
       :columns="columns"
       :data-source="data"
       :data-source="data"
@@ -45,35 +29,36 @@
       class="cooperation-table modal-table"
       class="cooperation-table modal-table"
     />
     />
 
 
-    <!-- 底部按钮 -->
     <div class="modal-footer ant-modal-footer">
     <div class="modal-footer ant-modal-footer">
-      <a-button @click="visible = false">取消</a-button>
-      <a-button type="primary" @click="handleConfirm">确定</a-button>
+      <a-button @click="visible = false">{{ t('common.cancel') }}</a-button>
+      <a-button type="primary" @click="handleConfirm">{{ t('common.comfirm') }}</a-button>
     </div>
     </div>
   </a-modal>
   </a-modal>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, h, } from "vue";
+import { ref, h, computed } from "vue";
 import { message } from "ant-design-vue";
 import { message } from "ant-design-vue";
+import { useI18n } from "vue-i18n";
 import { cooperation, saveBatch, sceneDetail } from "@/api/scene";
 import { cooperation, saveBatch, sceneDetail } from "@/api/scene";
 import dayjs from 'dayjs'
 import dayjs from 'dayjs'
+
+const { t } = useI18n();
 const visible = ref(false);
 const visible = ref(false);
 
 
-// 表格列配置
-const columns = [
+const columns = computed(() => [
   {
   {
-    title: "账号",
+    title: t('scene.cooperationModal.account'),
     dataIndex: "userName",
     dataIndex: "userName",
     key: "userName",
     key: "userName",
   },
   },
   {
   {
-    title: "协作时间",
+    title: t('scene.cooperationModal.cooperationTime'),
     dataIndex: "time",
     dataIndex: "time",
     key: "time",
     key: "time",
   },
   },
   {
   {
-    title: "操作",
+    title: t('common.operate'),
     key: "action",
     key: "action",
     customRender: (record) =>
     customRender: (record) =>
       h(
       h(
@@ -81,14 +66,13 @@ const columns = [
         {
         {
           class: "text-primary",
           class: "text-primary",
           style: { fontSize: "14px", color: 'var(--ant-primary-color)' },
           style: { fontSize: "14px", color: 'var(--ant-primary-color)' },
-          onClick: () => alert("点击"),
+          onClick: () => handleCancelCollab(record),
         },
         },
-        '取消协作'
+        t('scene.cooperationModal.cancelCooperation')
       ),
       ),
   },
   },
-];
+]);
 
 
-// 模拟数据
 const data = ref([]);
 const data = ref([]);
 const apiData = ref({
 const apiData = ref({
   type: 'laser',
   type: 'laser',
@@ -102,23 +86,16 @@ const cooperationInfo = ref({
   freeCount: 1,
   freeCount: 1,
 });
 });
 const userName = ref("");
 const userName = ref("");
+
 function fetch(value, callback) {
 function fetch(value, callback) {
   callback([
   callback([
     {
     {
       value: "138xxxxxxxx",
       value: "138xxxxxxxx",
       label: "138xxxxxxxx",
       label: "138xxxxxxxx",
     },
     },
-  ]); // 模拟异步请求数据
+  ]);
 }
 }
-const handleSearch = (val) => {
-  fetch(val, (d) => (data.value = d));
-};
-const handleChange = (val) => {
-  console.log(val);
-  value.value = val;
-  fetch(val, (d) => (data.value = d));
-};
-// 打开弹窗方法(父组件调用)
+
 const openModal = async (selectData) => {
 const openModal = async (selectData) => {
   visible.value = true;
   visible.value = true;
   if (Array.isArray(selectData)) {
   if (Array.isArray(selectData)) {
@@ -136,10 +113,9 @@ const openModal = async (selectData) => {
 };
 };
 
 
 const initData = async () => {
 const initData = async () => {
-  // 初始化数据逻辑
   cooperationInfo.value = await cooperation();
   cooperationInfo.value = await cooperation();
 };
 };
-// 添加协作
+
 const handleAdd = () => {
 const handleAdd = () => {
   data.value.push({
   data.value.push({
     userName: userName.value,
     userName: userName.value,
@@ -148,15 +124,13 @@ const handleAdd = () => {
   userName.value  = "";
   userName.value  = "";
 };
 };
 
 
-// 取消协作
 const handleCancelCollab = (record) => {
 const handleCancelCollab = (record) => {
-  message.success(`已取消与 ${record.userName} 的协作`);
+  message.success(t('scene.cooperationModal.cancelSuccess', { userName: record.userName }));
 };
 };
 
 
-// 确定
 const handleConfirm = async () => {
 const handleConfirm = async () => {
   await saveBatch({...apiData.value, userNameList: data.value.map(item => item.userName)});
   await saveBatch({...apiData.value, userNameList: data.value.map(item => item.userName)});
-  message.success("操作已确认");
+  message.success(t('scene.cooperationModal.confirmed'));
   visible.value = false;
   visible.value = false;
 };
 };
 
 
@@ -166,7 +140,7 @@ defineExpose({ openModal });
 <style scoped lang="less">
 <style scoped lang="less">
 .modal-header {
 .modal-header {
   display: flex;
   display: flex;
-  gap: 16px; /* 左右之间的间距 */
+  gap: 16px;
   align-items: center;
   align-items: center;
   height: 44px;
   height: 44px;
   width: 100%;
   width: 100%;
@@ -184,4 +158,4 @@ defineExpose({ openModal });
 .text-primary{
 .text-primary{
   color: @primary-color
   color: @primary-color
 }
 }
-</style>
+</style>

+ 32 - 66
src/views/scene/components/moveSceneModal.vue

@@ -1,14 +1,12 @@
 <template>
 <template>
   <a-modal
   <a-modal
     v-model:open="visible"
     v-model:open="visible"
-    title="移动场景"
+    :title="t('scene.moveSceneModal.title')"
     :width="600"
     :width="600"
     :footer="null"
     :footer="null"
     class="move-modal"
     class="move-modal"
   >
   >
-    <!-- 文件夹树形区域 -->
     <div class="folder-container">
     <div class="folder-container">
-      <!-- 有数据:树形组件 -->
       <a-tree
       <a-tree
         v-if="treeData.length > 0"
         v-if="treeData.length > 0"
         :tree-data="treeData"
         :tree-data="treeData"
@@ -20,11 +18,9 @@
         @expand="handleExpand"
         @expand="handleExpand"
         class="custom-tree"
         class="custom-tree"
       >
       >
-        <template #icon><FolderOpenTwoTone /></template> 
-        <!-- 自定义节点渲染:图标 + 文字/输入框 + 操作按钮 -->
+        <template #icon><FolderOpenTwoTone /></template>
         <template #title="{ data }">
         <template #title="{ data }">
           <div class="tree-node">
           <div class="tree-node">
-            <!-- 文件夹名称 / 编辑输入框 -->
             <template v-if="editingId === data.id">
             <template v-if="editingId === data.id">
               <a-input
               <a-input
                 v-model:value="editName"
                 v-model:value="editName"
@@ -38,7 +34,6 @@
             </template>
             </template>
             <span v-else class="node-text">{{ data.name }}</span>
             <span v-else class="node-text">{{ data.name }}</span>
 
 
-            <!-- 选中后展示操作按钮 -->
             <div class="node-actions" v-if="selectedId === data.id">
             <div class="node-actions" v-if="selectedId === data.id">
               <template v-if="editingId === data.id">
               <template v-if="editingId === data.id">
                   <check-outlined style="color: #52c41a"  @click="confirmEdit(data)" />
                   <check-outlined style="color: #52c41a"  @click="confirmEdit(data)" />
@@ -53,24 +48,23 @@
         </template>
         </template>
       </a-tree>
       </a-tree>
 
 
-      <!-- 空数据状态 -->
-      <empty-data class="empty-state" text="暂无文件夹,请点击【新建文件夹】创建" v-else />
+      <empty-data class="empty-state" :text="t('scene.moveSceneModal.emptyFolder')" v-else />
     </div>
     </div>
 
 
-    <!-- 底部按钮 -->
     <div class="modal-footer ant-modal-footer">
     <div class="modal-footer ant-modal-footer">
-      <a-button @click="handleCreate">新建文件夹</a-button>
+      <a-button @click="handleCreate">{{ t('scene.sceneAdmin.createFolder') }}</a-button>
       <div class="btn-group">
       <div class="btn-group">
-        <a-button @click="visible = false">取消</a-button>
-        <a-button type="primary" @click="handleConfirm">确定</a-button>
+        <a-button @click="visible = false">{{ t('common.cancel') }}</a-button>
+        <a-button type="primary" @click="handleConfirm">{{ t('common.comfirm') }}</a-button>
       </div>
       </div>
     </div>
     </div>
   </a-modal>
   </a-modal>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, nextTick, onMounted, watch, defineEmits, defineProps } from 'vue'
-import { message, Modal } from 'ant-design-vue'
+import { ref, nextTick, onMounted, watch } from 'vue'
+import { message } from 'ant-design-vue'
+import { useI18n } from 'vue-i18n'
 import emptyData from '@/components/EmptyData/index.vue'
 import emptyData from '@/components/EmptyData/index.vue'
 import { folderList, folderMove, updateFolder, folderDelete } from '@/api/scene'
 import { folderList, folderMove, updateFolder, folderDelete } from '@/api/scene'
 import {
 import {
@@ -81,6 +75,7 @@ import {
   CloseOutlined
   CloseOutlined
 } from '@ant-design/icons-vue'
 } from '@ant-design/icons-vue'
 
 
+const { t } = useI18n();
 const visible = ref(false)
 const visible = ref(false)
 const editInputRef = ref(null)
 const editInputRef = ref(null)
 const apiData = ref({
 const apiData = ref({
@@ -90,57 +85,50 @@ const apiData = ref({
   parentId: ''
   parentId: ''
 })
 })
 
 
-// 树形数据结构 (children 实现层级嵌套)
 const treeData = ref([])
 const treeData = ref([])
 onMounted(()=>{
 onMounted(()=>{
   getList()
   getList()
 })
 })
-// 树形状态
-const selectedId = ref(null)    // 选中节点ID
-const expandedKeys = ref([1])   // 默认展开的节点
-const editingId = ref(null)     // 正在编辑的节点ID
-const editName = ref('')        // 编辑临时名称
 
 
-const emit = defineEmits(['refresh'])
+const selectedId = ref(null)
+const expandedKeys = ref([1])
+const editingId = ref(null)
+const editName = ref('')
 
 
-// 树节点选中
-const handleTreeSelect = (keys, e) => {
-  // 编辑中禁止切换选中
-  console.log('keys', keys, e)
+const emit = defineEmits(['refresh'])
 
 
+const handleTreeSelect = (keys) => {
   if (editingId.value) return
   if (editingId.value) return
   selectedId.value = keys[0] ?? null
   selectedId.value = keys[0] ?? null
 }
 }
+
 const getList = () => {
 const getList = () => {
   folderList({type: 'laser', folderIdList: [],}).then(res => {
   folderList({type: 'laser', folderIdList: [],}).then(res => {
-    console.log('folderList', res)
     treeData.value = res
     treeData.value = res
   })
   })
 }
 }
-// 树节点展开/收起
+
 const handleExpand = (keys) => {
 const handleExpand = (keys) => {
   expandedKeys.value = keys
   expandedKeys.value = keys
 }
 }
 
 
-// 新建文件夹 (默认添加到根节点)
 const handleCreate = () => {
 const handleCreate = () => {
   const newId = Date.now()
   const newId = Date.now()
+  const folderName = t('scene.moveSceneModal.newFolder')
   treeData.value.push({
   treeData.value.push({
     id: newId,
     id: newId,
-    name: '新建文件夹',
+    name: folderName,
     children: []
     children: []
   })
   })
-  // 自动选中 + 进入编辑
   selectedId.value = newId
   selectedId.value = newId
   editingId.value = newId
   editingId.value = newId
-  editName.value = '新建文件夹'
+  editName.value = folderName
 
 
   nextTick(() => {
   nextTick(() => {
     editInputRef.value?.focus()
     editInputRef.value?.focus()
   })
   })
 }
 }
 
 
-// 开始编辑
 const startEdit = (data) => {
 const startEdit = (data) => {
   editingId.value = data.id
   editingId.value = data.id
   editName.value = data.name
   editName.value = data.name
@@ -149,15 +137,13 @@ const startEdit = (data) => {
   })
   })
 }
 }
 
 
-// 确认编辑名称
 const confirmEdit = async (item) => {
 const confirmEdit = async (item) => {
   const name = editName.value.trim()
   const name = editName.value.trim()
   if (!name) {
   if (!name) {
-    message.warning('文件夹名称不能为空')
+    message.warning(t('scene.sceneAdmin.folderModule.emptyFolderName'))
     return
     return
   }
   }
 
 
-  // 递归更新树形节点名称
   const updateNodeName = (list) => {
   const updateNodeName = (list) => {
     list.forEach(node => {
     list.forEach(node => {
       if (node.id === editingId.value) {
       if (node.id === editingId.value) {
@@ -173,13 +159,11 @@ const confirmEdit = async (item) => {
   editingId.value = null
   editingId.value = null
 }
 }
 
 
-// 取消编辑
-const cancelEdit = async (item) => {
+const cancelEdit = async () => {
   editingId.value = null
   editingId.value = null
   editName.value = ''
   editName.value = ''
 }
 }
 
 
-// 删除文件夹 (递归删除树形节点)
 const handleDelete = async (data) => {
 const handleDelete = async (data) => {
   const delId = data.id
   const delId = data.id
   const deleteNode = (list) => {
   const deleteNode = (list) => {
@@ -195,36 +179,33 @@ const handleDelete = async (data) => {
     return false
     return false
   }
   }
   deleteNode(treeData.value)
   deleteNode(treeData.value)
-  await folderDelete({id: data.id}) // 重新获取列表,恢复原始名称
+  await folderDelete({id: data.id})
 
 
   selectedId.value = null
   selectedId.value = null
-  message.success('删除成功')
+  message.success(t('toast.37'))
   emit('refresh')
   emit('refresh')
-
 }
 }
 
 
-// 确定按钮
 const handleConfirm = async () => {
 const handleConfirm = async () => {
-  console.log('selectedId', selectedId.value)
   if (!selectedId.value) {
   if (!selectedId.value) {
-    message.warning('请选择目标文件夹')
+    message.warning(t('scene.sceneAdmin.folderModule.selectTargetFolderTip'))
     return
     return
   }
   }
   apiData.value.parentId = selectedId.value
   apiData.value.parentId = selectedId.value
   await folderMove(apiData.value)
   await folderMove(apiData.value)
-  message.success(`已移动到目标文件夹`)
+  message.success(t('scene.sceneAdmin.folderModule.moveSuccessTip'))
   emit('refresh')
   emit('refresh')
   visible.value = false
   visible.value = false
 }
 }
-const openModal = (data) => {
-  const { list = [], type = 'laser' } = data
 
 
-  console.log('openModal', list)
+const openModal = (data) => {
+  const { list = [], type = 'laser' } = data || {}
   apiData.value.type = type
   apiData.value.type = type
   apiData.value.numList = list.filter(item => item.num).map(item => item.num)
   apiData.value.numList = list.filter(item => item.num).map(item => item.num)
   apiData.value.folderIdList = list.filter(item => item.tbType == 'f').map(item => item.id)
   apiData.value.folderIdList = list.filter(item => item.tbType == 'f').map(item => item.id)
   visible.value = true;
   visible.value = true;
 };
 };
+
 watch(() => visible.value, (val) => {
 watch(() => visible.value, (val) => {
   if (val) {
   if (val) {
     selectedId.value = null
     selectedId.value = null
@@ -233,7 +214,7 @@ watch(() => visible.value, (val) => {
     getList()
     getList()
   }
   }
 })
 })
-// 对外暴露属性,供父组件调用
+
 defineExpose({
 defineExpose({
   openModal,
   openModal,
   visible,
   visible,
@@ -254,7 +235,6 @@ defineExpose({
   height: 584px;
   height: 584px;
   overflow-y: auto;
   overflow-y: auto;
 
 
-  // 树形全局样式
   :deep(.ant-tree) {
   :deep(.ant-tree) {
     background: transparent;
     background: transparent;
     .ant-tree-switcher{
     .ant-tree-switcher{
@@ -279,12 +259,10 @@ defineExpose({
     display: flex;
     display: flex;
     padding: 13px 10px 13px 15px;
     padding: 13px 10px 13px 15px;
     font-size: 14px;
     font-size: 14px;
-
     align-items: center;
     align-items: center;
     flex: 1;
     flex: 1;
   }
   }
   
   
-  // 自定义树节点行
   .tree-node {
   .tree-node {
     display: flex;
     display: flex;
     align-items: center;
     align-items: center;
@@ -305,24 +283,12 @@ defineExpose({
     }
     }
   }
   }
 
 
-  // 空状态
   .empty-state {
   .empty-state {
     display: flex;
     display: flex;
     flex-direction: column;
     flex-direction: column;
     align-items: center;
     align-items: center;
     justify-content: center;
     justify-content: center;
     height: 300px;
     height: 300px;
-
-    .empty-img {
-      width: 80px;
-      margin-bottom: 12px;
-    }
-
-    .empty-text {
-      color: #999;
-      font-size: 14px;
-      margin: 0;
-    }
   }
   }
 }
 }
 
 
@@ -337,4 +303,4 @@ defineExpose({
     gap: 12px;
     gap: 12px;
   }
   }
 }
 }
-</style>
+</style>

+ 7 - 10
src/views/scene/components/renameModal.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
   <a-modal
   <a-modal
-    title="重命名"
+    :title="t('scene.renameModal.title')"
     :open="visible"
     :open="visible"
     @cancel="handleCancel"
     @cancel="handleCancel"
     @ok="handleConfirm"
     @ok="handleConfirm"
@@ -8,7 +8,7 @@
   >
   >
     <a-input
     <a-input
       v-model:value="localName"
       v-model:value="localName"
-      placeholder="请输入文件名称"
+      :placeholder="t('scene.renameModal.placeholder')"
       style="margin: 0 0 24px 0;padding: 9px 11px"
       style="margin: 0 0 24px 0;padding: 9px 11px"
       size="large"
       size="large"
       @keyup.enter="handleConfirm"
       @keyup.enter="handleConfirm"
@@ -17,9 +17,12 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, defineEmits, defineProps, watch } from 'vue'
+import { ref, watch } from 'vue'
+import { useI18n } from 'vue-i18n'
 import { updateFolder } from '@/api/scene'
 import { updateFolder } from '@/api/scene'
 
 
+const { t } = useI18n();
+
 const props = defineProps({
 const props = defineProps({
   visible: {
   visible: {
     type: Boolean,
     type: Boolean,
@@ -33,12 +36,9 @@ const props = defineProps({
 
 
 const emit = defineEmits(['update:visible', 'confirm'])
 const emit = defineEmits(['update:visible', 'confirm'])
 
 
-// 本地变量,用于双向绑定输入框
 const localName = ref('')
 const localName = ref('')
 
 
-// 弹窗打开/关闭时同步初始值
 watch(() => props.visible, (val) => {
 watch(() => props.visible, (val) => {
-  console.log('props.item', props.item)
   if (val) {
   if (val) {
     localName.value = props.item.title || ''
     localName.value = props.item.title || ''
   } else {
   } else {
@@ -47,7 +47,6 @@ watch(() => props.visible, (val) => {
 }, { immediate: true })
 }, { immediate: true })
 
 
 const handleCancel = () => {
 const handleCancel = () => {
-  // 通知父组件关闭弹窗
   emit('update:visible', false)
   emit('update:visible', false)
 }
 }
 
 
@@ -55,9 +54,7 @@ const handleConfirm = async () => {
   const name = localName.value.trim()
   const name = localName.value.trim()
   if (!name) return
   if (!name) return
   await updateFolder({...props.item, name, title: name})
   await updateFolder({...props.item, name, title: name})
-  // 通知父组件:确认重命名
   emit('confirm', name)
   emit('confirm', name)
-  // 关闭弹窗
   emit('update:visible', false)
   emit('update:visible', false)
 }
 }
 </script>
 </script>
@@ -68,4 +65,4 @@ const handleConfirm = async () => {
   justify-content: flex-end;
   justify-content: flex-end;
   gap: 12px;
   gap: 12px;
 }
 }
-</style>
+</style>

+ 23 - 17
src/views/scene/components/uploadModal.vue

@@ -1,7 +1,7 @@
 <template>
 <template>
   <a-modal
   <a-modal
     v-model:open="visible"
     v-model:open="visible"
-    title="上传文件"
+    :title="t('scene.uploadModal.title')"
     :footer="null"
     :footer="null"
     style="padding: 26px"
     style="padding: 26px"
     :width="1148"
     :width="1148"
@@ -24,15 +24,13 @@
           @change="handleUploadChange"
           @change="handleUploadChange"
           class="custom-upload"
           class="custom-upload"
         >
         >
-          <div class="upload-area cursor-pointer">
+          <div class="cursor-pointer upload-area">
             <div class="upload-text">
             <div class="upload-text">
-              将文件拖到此处,或
-              <a href="javascript:;" class="upload-link">点击上传</a>
+              {{ t('scene.uploadModal.dragTip') }}
+              <a href="javascript:;" class="upload-link">{{ t('scene.uploadModal.clickUpload') }}</a>
             </div>
             </div>
             <div class="upload-tip">
             <div class="upload-tip">
-              支持 E57、LAS、PLY
-              格式的点云上传支持四维看看系列相机导出的原始数据上传, 文件格式为
-              ZIP
+              {{ t('scene.uploadModal.formatTip') }}
             </div>
             </div>
           </div>
           </div>
         </a-upload>
         </a-upload>
@@ -42,13 +40,13 @@
       <div class="upload-right">
       <div class="upload-right">
         <!-- 上传中提示 -->
         <!-- 上传中提示 -->
         <div v-if="uploadList.length && isUploading" class="upload-tip-bar">
         <div v-if="uploadList.length && isUploading" class="upload-tip-bar">
-          正在上传, 请勿关闭此页面
+          {{ t('scene.uploadModal.uploadingTip') }}
         </div>
         </div>
 
 
         <!-- 文件列表 -->
         <!-- 文件列表 -->
         <div class="file-list" v-if="uploadList.length > 0">
         <div class="file-list" v-if="uploadList.length > 0">
           <div
           <div
-            class="file-item flex align-center"
+            class="flex file-item align-center"
             v-for="(file,index) in uploadList"
             v-for="(file,index) in uploadList"
             :key="file.uid"
             :key="file.uid"
           >
           >
@@ -70,14 +68,14 @@
             "
             "
               />
               />
             </div>
             </div>
-            <div class="file-actions flex align-center">
+            <div class="flex file-actions align-center">
               <a-button
               <a-button
                 v-if="file.status === 'error'"
                 v-if="file.status === 'error'"
                 type="link"
                 type="link"
                 size="small"
                 size="small"
                 @click="retryUpload(file)"
                 @click="retryUpload(file)"
               >
               >
-                续传
+                {{ t('scene.uploadModal.resume') }}
               </a-button>
               </a-button>
               <close-outlined @click="handleRemoveFile(index)" style="flex: 0 0 auto" />
               <close-outlined @click="handleRemoveFile(index)" style="flex: 0 0 auto" />
             </div>
             </div>
@@ -90,11 +88,11 @@
     </div>
     </div>
 
 
     <!-- 底部:复选框 + 按钮 -->
     <!-- 底部:复选框 + 按钮 -->
-    <div class="modal-footer ant-modal-footer justify-end" style="justify-content: end;">
+    <div class="justify-end modal-footer ant-modal-footer" style="justify-content: end;">
       <!-- <a-checkbox v-model:checked="saveToMediaLib">保存到媒体库</a-checkbox> -->
       <!-- <a-checkbox v-model:checked="saveToMediaLib">保存到媒体库</a-checkbox> -->
       <div class="btn-group">
       <div class="btn-group">
-        <a-button @click="visible = false">取消</a-button>
-        <a-button type="primary" :disabled="isUploading" @click="handleConfirm">确定</a-button>
+        <a-button @click="visible = false">{{ t('common.cancel') }}</a-button>
+        <a-button type="primary" :disabled="isUploading" @click="handleConfirm">{{ t('common.comfirm') }}</a-button>
       </div>
       </div>
     </div>
     </div>
   </a-modal>
   </a-modal>
@@ -103,6 +101,7 @@
 <script setup>
 <script setup>
 import { ref } from "vue";
 import { ref } from "vue";
 import { message } from "ant-design-vue";
 import { message } from "ant-design-vue";
+import { useI18n } from "vue-i18n";
 import { CloseOutlined } from "@ant-design/icons-vue";
 import { CloseOutlined } from "@ant-design/icons-vue";
 import emptyData from "@/components/EmptyData/index.vue";
 import emptyData from "@/components/EmptyData/index.vue";
 import { useUserStore } from "@/stores";
 import { useUserStore } from "@/stores";
@@ -110,8 +109,15 @@ import { relevanceE57 } from "@/api/scene";
 import { autoFormatKbSplit } from "@/utils/index";
 import { autoFormatKbSplit } from "@/utils/index";
 import { useOssUpload } from "@/hooks/useOssUpload";
 import { useOssUpload } from "@/hooks/useOssUpload";
 
 
+const { t } = useI18n();
 const emit = defineEmits(["refresh"]);
 const emit = defineEmits(["refresh"]);
 
 
+const props = defineProps({       
+  isObj: {
+    type: String,
+    default: "0",
+  },
+});
 const userStore = useUserStore();
 const userStore = useUserStore();
 const visible = ref(false);
 const visible = ref(false);
 
 
@@ -134,17 +140,17 @@ const handleRemoveFile = removeFile;
 
 
 const handleConfirm = async () => {
 const handleConfirm = async () => {
   if (uploadList.value.length === 0) {
   if (uploadList.value.length === 0) {
-    message.warning("请先上传文件");
+    message.warning(t("scene.uploadModal.uploadFirst"));
     return;
     return;
   }
   }
   const reqBody = uploadList.value.map((item, index) => ({
   const reqBody = uploadList.value.map((item, index) => ({
     id: index,
     id: index,
     title: item.name,
     title: item.name,
-    isObj: 1,
+    isObj: props.isObj,
     newFileName: item.newFileName,
     newFileName: item.newFileName,
   }));
   }));
   await relevanceE57(reqBody);
   await relevanceE57(reqBody);
-  message.success("已提交上传任务");
+  message.success(t("scene.uploadModal.submitSuccess"));
   clearUploadList();
   clearUploadList();
   emit("refresh");
   emit("refresh");
   visible.value = false;
   visible.value = false;

+ 693 - 26
src/views/scene/mesh.vue

@@ -1,37 +1,704 @@
+<script setup>
+import { ref, h, watch, onMounted, createVNode } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import { useI18n } from "vue-i18n";
+import { Modal, message } from "ant-design-vue";
+import {
+  QuestionCircleOutlined,
+  ExclamationCircleFilled,
+  SearchOutlined,
+  DownloadOutlined,
+  MoreOutlined,
+  EditOutlined,
+  CheckCircleFilled,
+} from "@ant-design/icons-vue";
+import { sceneList, copyScene, sceneDetail, buildSceneObj, getTakeLookRoomId } from "@/api/scene";
+import BaseTable from "@/components/BaseTable/index.vue";
+import cooperation from "./components/cooperation.vue";
+import uploadModal from "./components/uploadModal.vue";
+import moveSceneModal from "./components/moveSceneModal.vue";
+import renameModal from "./components/renameModal.vue";
+import { useSceneStore, useUserStore } from "@/stores";
+import {
+  objColumns,
+  getStatusImg,
+  getStatusText,
+  SceneTypePaths,
+  getCameraType,
+} from "./data";
+const { t } = useI18n();
+const router = useRouter();
+const useScene = useSceneStore();
+const userStore = useUserStore();
+
+const createRoomId = () =>
+  typeof crypto !== "undefined" && crypto.randomUUID
+    ? crypto.randomUUID()
+    : `${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
+const activeKey = ref("0");
+const isObj = ref(1);
+const tablist = ref([
+  {
+    label: t("scene.myScene"),
+    value: "0",
+    key: "0",
+  },
+  {
+    label: t("scene.cooperaScene"),
+    value: "1",
+    key: "1",
+  },
+]);
+
+// 仅需维护这4个变量
+const baseTableRef = ref();
+const snKey = ref("");
+const numKey = ref([]);
+const selectTable = ref([]);
+const cameraType = ref(null);
+const folderId = ref(null);
+const folderPath = ref([
+  {
+    name: t("scene.gml"),
+    id: 0,
+    index: 0,
+  },
+]);
+const reName = ref({
+  show: false,
+  name: "",
+  item: null,
+});
+const search = ref({
+  key: "",
+  type: "name",
+});
+
+const tableList = ref([]);
+const loading = ref(false);
+const cooperationRef = ref(null);
+const moveModalRef = ref(null);
+const uploadModalRef = ref(null);
+const total = ref(0);
+const options = [...Array(25)].map((_, i) => ({
+  value: (i + 10).toString(36) + (i + 1),
+}));
+
+const rowSelection = {
+  onChange: (selectedRowKeys, selectedRows) => {
+    console.log(
+      `selectedRowKeys: ${selectedRowKeys}`,
+      "selectedRows:",
+      selectedRows
+    );
+    let newKey = selectedRows.map((item) => item.num);
+    numKey.value = newKey.filter((item) => item != null);
+    console.log(numKey.value, "numKey");
+    selectTable.value = selectedRows;
+  },
+  getCheckboxProps: (record) => ({
+    disabled: record.tbType === "f" ? false : record.status != -2, // Column configuration not to be checked
+    name: record.name,
+  }),
+};
+// 接口查询(只接收组件抛出来的分页参数)
+const getList = async ({ pageNum, pageSize, current }) => {
+  loading.value = true;
+  let apiData = {
+    pageNum: current || pageNum,
+    pageSize,
+    isObj: isObj.value,
+    title: (search.value.type == "name" && search.value.key) || "",
+    sn: (search.value.type == "sn" && search.value.key) || "",
+    num: (search.value.type == "num" && search.value.key) || "",
+    // folderId: folderId.value,
+    labType: activeKey.value,
+    cameraType: cameraType.value,
+  };
+  const res = await sceneList(apiData);
+  tableList.value = res.list;
+  total.value = res.total;
+  loading.value = false;
+};
+
+// 点击搜索:重置页码到第1页再查询
+const searchClick = () => {
+  selectTable.value = [];
+  baseTableRef.value.resetPage();
+};
+const handleItem = async (record, item) => {
+  switch (record.key) {
+    case "copy":
+      let { responseUserIncrement } = await useScene.checkCamera(item.snCode);
+      if (responseUserIncrement && responseUserIncrement.isExpired == 0) {
+        Modal.confirm({
+          title: t("common.tip"),
+          icon: createVNode(ExclamationCircleFilled),
+          content: t("scene.myScenes.copyTips"),
+          centered: true,
+          // okText: '确认',
+          // cancelText: '取消',
+          onOk: async () => {
+            copyaApi(item);
+          },
+        });
+      } else {
+        Modal.confirm({
+          title: t("common.tip"),
+          icon: createVNode(ExclamationCircleFilled),
+          content: t("scene.myScenes.copyErr"),
+          centered: true,
+          // okText: '确认',
+          // cancelText: '取消',
+          onOk: async () => {
+            router.push({ path: "/mall/index" });
+          },
+        });
+      }
+      break;
+    case "daikan":
+      handleLivestream(item)
+      break;
+    case "more":
+      useScene.updataCart(item)
+      router.push({ path: "/sceneomore/mesh/" + item.num });
+      break;
+    case "offline":
+      useScene.updataCart(item)
+      router.push({ path: "/sceneomore/mesh/" + item.num });
+      break;
+    case "collaborativeScene":
+      handleEditItem(record);
+      break;
+  }
+  console.log(record, "handleItem", record, item);
+};
+const handleLivestream = async (item) => {
+  let roomId = createRoomId();
+  try {
+    const roomRes = await getTakeLookRoomId({ num: item.num });
+    if (roomRes?.code === 0 && roomRes?.data?.roomId) {
+      roomId = roomRes.data.roomId;
+    }
+  } catch {
+    // 接口失败时使用本地 roomId
+  }
+
+  try {
+    const data = await useScene.checkCamera(item.snCode);
+    const { responseUserIncrement } = data || {};
+    if (responseUserIncrement && responseUserIncrement.isExpired == 0) {
+      let url = item.webSite;
+      if (!url) {
+        message.warning(t("common.fail"));
+        return;
+      }
+      let page = url.substring(url.lastIndexOf("/") + 1, url.lastIndexOf(".html"));
+      if (item.isUpgrade == 1) {
+        page = url.substring(url.lastIndexOf("/") + 1, url.lastIndexOf(".html") + 5);
+        url = url.replace(page, "livestream/");
+        url =
+          url +
+          `&role=leader&name=${userStore.nickName}&roomId=${roomId}&avatar=${encodeURIComponent(userStore.head || "")}&vruserId=user_${createRoomId()}&isTour=0`;
+      } else {
+        url = url.replace(page, "rtc-live");
+      }
+      const langParam = userStore.lang !== "zh" ? "&lang=en" : "";
+      window.open(url.replace("http://", "https://") + langParam, "_blank");
+      return;
+    }
+    Modal.confirm({
+      title: t("common.tip"),
+      icon: createVNode(ExclamationCircleFilled),
+      content: t("scene.myScenes.copyErr"),
+      centered: true,
+      onOk: () => {
+        router.push({ path: "/mall/index" });
+      },
+    });
+  } catch {
+    // 错误提示由 http 拦截器统一处理
+  }
+};
+const handleSee = (record) => {
+  let path = SceneTypePaths[record.sceneSource];
+  window.open(path[0], "_blank");
+};
+const generateApi = async (num) => {
+  await buildSceneObj({num});
+  message.success(t("common.success"));
+};
+
+const handleEditItem = (record) => {
+  if (record.tbType == "f") {
+    // 修改文件夹
+    return handleRename(record);
+  }
+  let path = SceneTypePaths[record.sceneSource];
+  window.open(path[1], "_blank");
+};
+const hendleCooperation = (item) => {
+  console.log("handleFolder", selectTable.value);
+  if(numKey.value.length){
+    const isNorn = selectTable.value.some(
+      (item) => item.status == 2 || item.status == 0
+    );
+    if (isNorn) {
+      Modal.confirm({
+        title: t("common.tip"),
+        icon: createVNode(ExclamationCircleFilled),
+        content: t("scene.sceneAdmin.cooperationBlockedTip"),
+        centered: true,
+        // okText: '确认',
+        // cancelText: '取消',
+        onOk: async () => {
+          await userStore.logout();
+        },
+      });
+    }
+    cooperationRef.value.openModal(selectTable.value);
+
+  }else{
+    cooperationRef.value.openModal([item]);
+
+  }
+};
+const handleFolder = () => {
+  moveModalRef.value.openModal({ list: selectTable.value, type: "laser" });
+};
+const handleShowUpload = () => {
+  if (uploadModalRef.value) {
+    uploadModalRef.value.openModal();
+  }
+};
+/**
+ * 处理文件复制操作
+ * @param {*} item 待复制的文件项
+ */
+const copyaApi = async (item) => {
+  await copyScene({ sceneNum: item.num });
+  message.success(t("scene.myScenes.copySuccess"));
+  searchClick()
+};
+const handleUploadSuccess = () => {
+  searchClick()
+};
+const handleRename = (record) => {
+  reName.value = {
+    show: true,
+    name: record.title,
+    item: record,
+  };
+};
+const onRenameConfirm = (name) => {
+  console.log("改名成功");
+  reName.value.name = name;
+  baseTableRef.value.refresh();
+};
+const handlePathClick = (item, index) => {
+  console.log("handlePathClick", item, index);
+  // currentItem
+  folderId.value = item.id;
+  folderPath.value.splice(index + 1);
+};
+const handleFolderItem = (item) => {
+  let list = folderPath.value;
+  folderId.value = item.id;
+  list.push(item);
+  folderPath.value = list;
+};
+watch(
+  () => [activeKey.value, folderId.value, cameraType.value],
+  () => {
+    selectTable.value = [];
+    numKey.value = [];
+    searchClick();
+  }
+);
+
+watch(
+  () => [search.value.key, search.value.type],
+  () => {
+    if (search.value.key) {
+      searchClick();
+    }
+  }
+);
+onMounted(() => {
+  // 初始化默认查询,不传分页,组件内部自动1页
+  searchClick();
+  // getList({ pageNum: 1, pageSize: 20 });
+});
+</script>
 <template>
 <template>
-  <div class="layout-404">
-    <div class="_404">
-      <img :src="`${$cdn}images/404.png`" alt="">
-      <p>抱歉,您访问的页面出现了错误,请重新加载</p>
-      <div @click="$router.push({name:'home'})">返回首页</div>
+  <div class="scene-layout">
+    <div class="tab">
+      <a-tabs v-model:activeKey="activeKey" size="large" class="tablist">
+        <a-tab-pane v-for="item in tablist" :key="item.key" :tab="item.label" />
+      </a-tabs>
+    </div>
+    <div class="table w-full">
+      <BaseTable
+        :columns="objColumns"
+        :table-data="tableList"
+        :loading="loading"
+        :total="total"
+        :bordered="false"
+        rowKey="id"
+        :rowSelectionOptions="rowSelection"
+        default-page-size="20"
+        @search="getList"
+        ref="baseTableRef"
+      >
+        <!-- 顶部搜索插槽 -->
+        <template #header>
+          <div
+            class="flex items-center justify-between w-full mb-5 tableHeader"
+            v-if="selectTable.length == 0"
+          >
+            <div
+              class="flex items-center justify-start headerLeft gap-x-4 position-relative"
+            >
+              <a-input-group compact style="width: auto; margin-right: 10px">
+                <a-input
+                  v-model:value="search.key"
+                  style="width: 400px"
+                  size="large"
+                  :placeholder="$t('common.search')"
+                >
+                  <template #prefix><SearchOutlined /></template>
+                  <template #suffix>
+                    <div
+                      class="suffix"
+                      style="height: 22px; border-left: 1px solid #f2f3f5"
+                    >
+                      <a-select
+                        :bordered="false"
+                        v-model:value="search.type"
+                        size="small"
+                        style="
+                          width: 120px;
+                          height: 22px;
+                          padding: 0 0px 0 12px;
+                        "
+                      >
+                        <a-select-option value="name">{{
+                          $t("scene.searchType.name")
+                        }}</a-select-option>
+                        <a-select-option value="num">{{
+                          $t("scene.searchType.num")
+                        }}</a-select-option>
+                        <a-select-option value="sn">{{
+                          $t("scene.searchType.sn")
+                        }}</a-select-option>
+                      </a-select>
+                    </div>
+                  </template>
+                </a-input>
+              </a-input-group>
+              <a-space>
+                <a-select
+                  size="large"
+                  v-model:value="cameraType"
+                  :options="getCameraType"
+                  style="width: 200px; height: 44px"
+                />
+              </a-space>
+            </div>
+            <div class="flex items-center gap-4 headerRight">
+              <ul
+                class="folder-path"
+                v-show="folderPath && folderPath.length > 1"
+              >
+                <li
+                  v-for="(item, index) in folderPath"
+                  :key="item.id"
+                  :class="{ 'is-last': index === folderPath.length - 1 }"
+                >
+                  <span @click="handlePathClick(item, item.index)">{{
+                    item.name || item.title
+                  }}</span
+                  ><span
+                    style="margin: 0 6px"
+                    v-if="index !== folderPath.length - 1"
+                    >/</span
+                  >
+                </li>
+              </ul>
+              <a-button
+                type="primary"
+                style="width: 50px"
+                @click="handleShowUpload"
+                size="large"
+              >
+                <template #icon>
+                  <!-- <DownloadOutlined /> -->
+                  <icon-font
+                    type="icon-uploading_e57"
+                    style="font-size: 16px"
+                  />
+                </template>
+              </a-button>
+            </div>
+          </div>
+          <div
+            class="flex items-center justify-end w-full mb-5 tableHeader"
+            v-else
+          >
+            <a-space size="middle">
+              <!-- 协作 -->
+              <a-button
+                class="operation"
+                size="small"
+                v-if="numKey.length == selectTable.length"
+                @click="hendleCooperation"
+              >
+                <i class="iconfont icon-menu_cooperation1"></i>
+              </a-button>
+              <a-button class="operation" size="small" @click="handleFolder">
+                <i class="iconfont icon-menu_move"></i>
+              </a-button>
+              <a-button
+                class="operation"
+                size="small"
+                danger
+                @click="handleEditItem(record)"
+              >
+                <i class="iconfont icon-common_toolbar_delete"></i>
+              </a-button>
+            </a-space>
+          </div>
+        </template>
+        <!-- 操作列插槽 -->
+        <template #sceneName="{ record }">
+          <div
+            class="flex items-center cursor-pointer sceneName"
+            @click="handleSee(record)"
+          >
+            <div class="relative w-32 rounded-lg sceneleft h-22 shrink-0">
+              <img
+                v-if="record.tbType == 'f'"
+                @click.stop="handleFolderItem(record)"
+                src="/src/assets/image/scene/folder.png"
+                alt=""
+                class="w-32 mr-2 Folder h-22"
+              />
+              <img
+                v-else
+                :src="getStatusImg(record)"
+                alt=""
+                class="w-32 mr-2 rounded-lg h-22"
+              />
+
+              <div
+                class="absolute rounded-lg loading-hover"
+                v-if="record.tbType != 'f' && record.status != -2"
+              >
+                <span>{{ getStatusText(record) }}</span>
+              </div>
+            </div>
+            <div class="ml-4 sceneNameTight">
+              <div class="text-base sceneNameTitle" style="color: #1d2129">
+                {{ record.name || record.sceneName || record.title }}
+              </div>
+              <div
+                class="text-sm sceneTime"
+                v-if="record.tbType == 'f' && record.sceneNum"
+                style="margin-top: 6px; color: #86909c"
+              >
+                {{ $t("scene.sceneAdmin.sceneNumber") }}:{{
+                  record.sceneNum || 0
+                }}
+              </div>
+              <div
+                class="text-sm sceneTime"
+                style="margin-top: 6px; color: #86909c"
+              >
+                {{ t("scene.updatedAt", { time: record.createTime }) }}
+              </div>
+            </div>
+          </div>
+        </template>
+        <!-- 操作列插槽 -->
+        <template #operate="{ record }">
+          <a-space size="middle">
+            <a-button
+              class="operation"
+              size="small"
+              @click="handleEditItem(record)"
+              :icon="h(EditOutlined)"
+            >
+            </a-button>
+            <a-dropdown
+              v-if="record.tbType != 'f'"
+              placement="bottomRight"
+              :trigger="['click']"
+              :get-popup-container="(trigger) => trigger.parentNode"
+              :arrow="{ pointAtCenter: true }"
+            >
+              <a-button
+                class="operation"
+                size="small"
+                :icon="h(MoreOutlined)"
+              ></a-button>
+
+              <template #overlay>
+                <a-menu
+                  class="operationList"
+                  style="padding: 0px"
+                  @click="
+                    (e) => {
+                      handleItem(e, record);
+                    }
+                  "
+                >
+                  <a-menu-item style="width: 140px" key="copy">
+                    <div class="item">
+                      <i class="iconfont icon-menu_copy"></i> {{ t("scene.myScenes.share.copy") }}
+                    </div>
+                  </a-menu-item>
+                   <a-menu-item class="item" key="offline">
+                    <div class="item">
+                      <i class="iconfont icon-common_toolbar_more_download"></i> {{ t("scene.offlinePackAction") }}
+                    </div>
+                  </a-menu-item>
+                  <a-menu-item class="item" key="daikan">
+                    <div class="item">
+                      <i class="iconfont icon-show"></i> {{ t("scene.myScenes.daikan") }}
+                    </div>
+                  </a-menu-item>
+                  <a-menu-item class="item" key="more">
+                    <div class="item">
+                      <i class="iconfont icon-look_flatten"></i> {{ t("scene.moreAction") }}
+                    </div>
+                  </a-menu-item>
+                </a-menu>
+              </template>
+            </a-dropdown>
+          </a-space>
+        </template>
+        <!-- 协作插槽 -->
+        <template #cooperation="{ record }">
+          <div class="cursor-pointer cooperation" @click="hendleCooperation(record)">
+            {{
+              record.cooperationUserList && record.cooperationUserList.length || 0
+            }}
+          </div>
+        </template>
+      </BaseTable>
+    </div>
+    <div class="moadl">
+      <cooperation ref="cooperationRef" />
+      <uploadModal isObj="0" ref="uploadModalRef" @refresh="handleUploadSuccess" />
+      <moveSceneModal ref="moveModalRef" @refresh="baseTableRef?.refresh" />
+      <renameModal
+        v-model:visible="reName.show"
+        :item="reName.item"
+        @confirm="onRenameConfirm"
+      />
     </div>
     </div>
   </div>
   </div>
 </template>
 </template>
 
 
 <style lang="less" scoped>
 <style lang="less" scoped>
-.layout-404{
+@import "./index.less";
+.scene-layout {
   position: relative;
   position: relative;
-  min-height: 90vh;
-  ._404{
-    top: 50%;
-    left: 50%;
-    position: absolute;
-    transform: translate(-50%,-50%);
-    text-align: center;
-    font-size: 16px;
-    color: #2d2d2d;
-    p{
-      margin: 30px 0;
-    }
-    div{
-      border: 1px solid #2d2d2d;
-      border-radius: 8px;
-      width: 120px;
-      line-height: 40px;
-      height: 40px;
-      margin: 0 auto;
-      cursor: pointer;
+  min-height: calc(90vh - 95px);
+  .tablist {
+    margin-top: 10px;
+    margin-bottom: 18px;
+  }
+  .tableHeader {
+    .folder-path {
+      display: flex;
+      li {
+        cursor: pointer;
+        color: #909090;
+        font-size: 14px;
+        margin-top: 10px;
+        &:hover,
+        &.is-last {
+          color: #323233;
+        }
+        &.is-last {
+          cursor: default;
+        }
+      }
+    }
+  }
+  .table {
+    .operation {
+      width: 55px;
+      height: 44px;
+      .anticon {
+        position: relative;
+        top: -3px;
+      }
+    }
+    .loading-hover {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      -webkit-transform: translate(-50%, -50%);
+      transform: translate(-50%, -50%);
+      top: 50%;
+      left: 50%;
+      line-height: 86px;
+      color: #fff;
+      background-color: rgba(0, 0, 0, 0.7);
+      text-align: center;
     }
     }
+    .sceneName {
+      .Folder {
+        width: 72px;
+        height: 56px;
+        margin: 16px 28px;
+      }
+      .sceneNameTitle {
+        font-weight: normal;
+        font-size: 14px;
+        color: #1d2129;
+        line-height: 22px;
+      }
+      .sceneTime {
+        font-size: 12px;
+        color: #86909c;
+        line-height: 20px;
+        text-align: left;
+        margin-top: 6px;
+      }
+    }
+  }
+}
+</style>
+<style lang="less">
+.scene-layout {
+  .tablist {
+    width: 100%;
+
+    .ant-tabs-ink-bar {
+      height: 3px; /* 下划线厚度,更醒目 */
+      width: 24px !important; /* 使下划线宽度与标签栏一致 */
+      transform: translate(34px, 0px);
+    }
+    .ant-tabs-tab {
+      font-weight: normal;
+      font-size: 24px;
+      color: #4e5969;
+      text-align: left;
+      padding: 10px 0;
+    }
+    .ant-tabs-nav {
+      &::before {
+        border: none;
+      }
+    }
+  }
+}
+.operationList {
+  .item {
+    padding: 3px 0;
   }
   }
 }
 }
 </style>
 </style>

+ 56 - 22
src/views/scene/more.vue

@@ -7,13 +7,13 @@
     <div class="content-wrapper">
     <div class="content-wrapper">
       <!-- 左侧:附加产物列表 -->
       <!-- 左侧:附加产物列表 -->
       <div class="left-section">
       <div class="left-section">
-        <h3 class="section-title">附加产物</h3>
+        <h3 class="section-title">{{ t('scene.sceneMore.additionalProducts') }}</h3>
 
 
         <!-- 产物1:生成 E57 -->
         <!-- 产物1:生成 E57 -->
-        <div class="product-item">
+        <div class="product-item" v-if="type == 'laser'">
           <div class="product-item-top">
           <div class="product-item-top">
             <div class="product-info">
             <div class="product-info">
-              <p class="product-name">生成 E57</p>
+              <p class="product-name">{{ t('scene.sceneMore.generateE57') }}</p>
             </div>
             </div>
             <div class="action-buttons">
             <div class="action-buttons">
               <a-button
               <a-button
@@ -35,11 +35,12 @@
           </div>
           </div>
           <div class="progress-wrap">
           <div class="progress-wrap">
             <template v-if="downe57Info.downloadUrl">
             <template v-if="downe57Info.downloadUrl">
-              计算完成时间:{{ sceneInfo.algorithmTime }} 过期时间:2026.04.15
+              {{ t('scene.sceneMore.calcCompleteTime', { time: sceneInfo.algorithmTime }) }}
+              {{ t('scene.sceneMore.expireTime', { time: '2026.04.15' }) }}
             </template>
             </template>
 
 
             <template v-else-if="downe57Info.buildE57Status !== 0">
             <template v-else-if="downe57Info.buildE57Status !== 0">
-              <span class="progress-text">{{downe57Info.downloadUrl?'已完成':'打包中'}}{{ downe57Info.progress }}%</span>
+              <span class="progress-text">{{ downe57Info.downloadUrl ? t('scene.sceneMore.packed') : t('scene.sceneMore.packing') }}{{ downe57Info.progress }}%</span>
               <a-progress
               <a-progress
                 style="flex: 1; margin-bottom: 0; margin-inline-end: 0"
                 style="flex: 1; margin-bottom: 0; margin-inline-end: 0"
                 :percent="downe57Info.progress"
                 :percent="downe57Info.progress"
@@ -55,7 +56,7 @@
         <div class="product-item">
         <div class="product-item">
           <div class="product-item-top">
           <div class="product-item-top">
             <div class="product-info">
             <div class="product-info">
-              <p class="product-name">离线包</p>
+              <p class="product-name">{{ t('scene.sceneMore.offlinePack') }}</p>
             </div>
             </div>
             <div class="action-buttons">
             <div class="action-buttons">
               <a-button class="operation" @click="handleDownScene" :disabled="downInfo.buildStatus == 2">
               <a-button class="operation" @click="handleDownScene" :disabled="downInfo.buildStatus == 2">
@@ -69,7 +70,7 @@
             </div>
             </div>
           </div>
           </div>
           <div class="progress-wrap" v-if="downInfo.buildStatus !== 0">
           <div class="progress-wrap" v-if="downInfo.buildStatus !== 0">
-              <span class="progress-text">{{downInfo.downloadUrl?'已完成':'打包中'}}{{ downInfo.progress }}%</span>
+              <span class="progress-text">{{ downInfo.downloadUrl ? t('scene.sceneMore.packed') : t('scene.sceneMore.packing') }}{{ downInfo.progress }}%</span>
               <a-progress
               <a-progress
                 style="flex: 1; margin-bottom: 0; margin-inline-end: 0"
                 style="flex: 1; margin-bottom: 0; margin-inline-end: 0"
                 :percent="downInfo.progress"
                 :percent="downInfo.progress"
@@ -83,10 +84,10 @@
 
 
       <!-- 右侧:场景信息 -->
       <!-- 右侧:场景信息 -->
       <div class="right-section">
       <div class="right-section">
-        <h3 class="section-title">场景信息</h3>
+        <h3 class="section-title">{{ t('scene.sceneMore.sceneInfo') }}</h3>
         <div class="info-list">
         <div class="info-list">
           <div class="info-item">
           <div class="info-item">
-            <span class="label">拍摄设备:</span>
+            <span class="label">{{ t('scene.sceneMore.shootDevice') }}</span>
             <span class="value"
             <span class="value"
               >{{ $t(`camera.device.${sceneInfo.cameraType}`) }} | SN:{{
               >{{ $t(`camera.device.${sceneInfo.cameraType}`) }} | SN:{{
                 sceneInfo.snCode
                 sceneInfo.snCode
@@ -94,33 +95,33 @@
             >
             >
           </div>
           </div>
           <div class="info-item">
           <div class="info-item">
-            <span class="label">拍摄方式:</span>
+            <span class="label">{{ t('scene.sceneMore.shootMode') }}</span>
             <span class="value">{{
             <span class="value">{{
               sceneInfo.location == 5 ||
               sceneInfo.location == 5 ||
               sceneInfo.location == 6 ||
               sceneInfo.location == 6 ||
               sceneInfo.location == 8
               sceneInfo.location == 8
-                ? "Slam"
-                : "架站式"
+                ? t('scene.sceneMore.slamMode')
+                : t('scene.sceneMore.tripodMode')
             }}</span>
             }}</span>
           </div>
           </div>
           <div class="info-item">
           <div class="info-item">
-            <span class="label">拍摄时间:</span>
+            <span class="label">{{ t('scene.sceneMore.shootTime') }}</span>
             <span class="value">{{ sceneInfo.createTime }}</span>
             <span class="value">{{ sceneInfo.createTime }}</span>
           </div>
           </div>
           <div class="info-item">
           <div class="info-item">
-            <span class="label">拍摄点位:</span>
+            <span class="label">{{ t('scene.sceneMore.shootPoints') }}</span>
             <span class="value">{{ sceneInfo.shootCount }}</span>
             <span class="value">{{ sceneInfo.shootCount }}</span>
           </div>
           </div>
           <div class="info-item">
           <div class="info-item">
-            <span class="label">拍摄地点:</span>
+            <span class="label">{{ t('scene.sceneMore.shootLocation') }}</span>
             <span class="value">{{ sceneInfo.addres || "-" }}</span>
             <span class="value">{{ sceneInfo.addres || "-" }}</span>
           </div>
           </div>
           <div class="info-item">
           <div class="info-item">
-            <span class="label">计算完成:</span>
+            <span class="label">{{ t('scene.sceneMore.calcFinish') }}</span>
             <span class="value">{{ sceneInfo.algorithmTime || "-" }}</span>
             <span class="value">{{ sceneInfo.algorithmTime || "-" }}</span>
           </div>
           </div>
           <div class="info-item">
           <div class="info-item">
-            <span class="label">计算时长:</span>
+            <span class="label">{{ t('scene.sceneMore.calcDuration') }}</span>
             <span class="value">{{
             <span class="value">{{
               (sceneInfo.computeTime &&
               (sceneInfo.computeTime &&
                 dayjsFormatHMS(sceneInfo.computeTime)) ||
                 dayjsFormatHMS(sceneInfo.computeTime)) ||
@@ -128,7 +129,7 @@
             }}</span>
             }}</span>
           </div>
           </div>
           <div class="info-item">
           <div class="info-item">
-            <span class="label">场景大小:</span>
+            <span class="label">{{ t('scene.sceneMore.sceneSize') }}</span>
             <span class="value">{{ autoFormatKbSplit(sceneInfo.space) }}</span>
             <span class="value">{{ autoFormatKbSplit(sceneInfo.space) }}</span>
           </div>
           </div>
         </div>
         </div>
@@ -144,7 +145,7 @@ import {
   QuestionCircleOutlined,
   QuestionCircleOutlined,
   ExclamationCircleFilled,
   ExclamationCircleFilled,
 } from "@ant-design/icons-vue";
 } from "@ant-design/icons-vue";
-import { ref, computed, onUnmounted } from "vue";
+import { ref, computed, onMounted, onUnmounted, createVNode } from "vue";
 import { useI18n } from "vue-i18n";
 import { useI18n } from "vue-i18n";
 import { useRouter, useRoute } from "vue-router";
 import { useRouter, useRoute } from "vue-router";
 import {
 import {
@@ -162,7 +163,7 @@ import {
   downloadProcess,
   downloadProcess,
 } from "@/api/scene";
 } from "@/api/scene";
 import { Modal, message } from "ant-design-vue";
 import { Modal, message } from "ant-design-vue";
-import { useSceneStore } from "@/stores";
+import { useSceneStore, useUserStore } from "@/stores";
 import dayjs from "dayjs";
 import dayjs from "dayjs";
 // 初始化路由、store
 // 初始化路由、store
 // 存储定时器ID
 // 存储定时器ID
@@ -193,10 +194,39 @@ const downInfo = ref({
 // 进度条百分比(可以和后端状态联动)
 // 进度条百分比(可以和后端状态联动)
 const progressPercent = ref(10);
 const progressPercent = ref(10);
 const useScene = useSceneStore();
 const useScene = useSceneStore();
+const userStore = useUserStore();
+
+const getRemainingDownloadCount = (info) => {
+  const total = Number(info?.downloadNumTotal ?? 0);
+  const used = Number(info?.downloadNum ?? 0);
+  return total - used;
+};
+
+const checkDownloadQuota = async () => {
+  const info = await userStore.getInfo();
+  const remaining = getRemainingDownloadCount(info || userStore);
+  if (remaining > 0) return true;
+
+  Modal.confirm({
+    title: t("common.tip"),
+    icon: createVNode(ExclamationCircleFilled),
+    content: t("scene.sceneMore.downloadInsufficient"),
+    centered: true,
+    onOk: () => {
+      router.push({
+        path: "/mall/sercice/2",
+        query: { sceneNum: num },
+      });
+    },
+  });
+  return false;
+};
 const getSceneInfo = async () => {
 const getSceneInfo = async () => {
   const info = await sceneDetail({ num, isObj: sceneInfo.value.isObj });
   const info = await sceneDetail({ num, isObj: sceneInfo.value.isObj });
   sceneInfo.value = info;
   sceneInfo.value = info;
-  handleCreateE57(true)
+  if(type == 'laser') {
+    handleCreateE57(true)
+  }
   handleDownScene(true)
   handleDownScene(true)
 };
 };
 const handleCreateE57 = async (down) => {
 const handleCreateE57 = async (down) => {
@@ -312,12 +342,16 @@ const stopPoll = () => {
     pollTimer = null;
     pollTimer = null;
   }
   }
 };
 };
+onMounted(async () => {
+  const hasQuota = await checkDownloadQuota();
+  if (!hasQuota) return;
+  await getSceneInfo();
+});
 // 组件销毁时强制清除定时器,离开页面不再请求
 // 组件销毁时强制清除定时器,离开页面不再请求
 onUnmounted(() => {
 onUnmounted(() => {
   stopPollE57();
   stopPollE57();
   stopPoll();
   stopPoll();
 });
 });
-getSceneInfo();
 </script>
 </script>
 
 
 <style scoped lang="less">
 <style scoped lang="less">

+ 23 - 23
src/views/scene/obj.vue

@@ -30,6 +30,7 @@ const { t } = useI18n();
 const router = useRouter();
 const router = useRouter();
 const useScene = useSceneStore();
 const useScene = useSceneStore();
 const activeKey = ref("0");
 const activeKey = ref("0");
+const isObj = ref(0);
 const tablist = ref([
 const tablist = ref([
   {
   {
     label: t("scene.myScene"),
     label: t("scene.myScene"),
@@ -100,7 +101,7 @@ const getList = async ({ pageNum, pageSize, current }) => {
   let apiData = {
   let apiData = {
     pageNum: current || pageNum,
     pageNum: current || pageNum,
     pageSize,
     pageSize,
-    isObj: 0,
+    isObj: isObj.value,
     title: (search.value.type == "name" && search.value.key) || "",
     title: (search.value.type == "name" && search.value.key) || "",
     sn: (search.value.type == "sn" && search.value.key) || "",
     sn: (search.value.type == "sn" && search.value.key) || "",
     num: (search.value.type == "num" && search.value.key) || "",
     num: (search.value.type == "num" && search.value.key) || "",
@@ -148,7 +149,6 @@ const handleItem = async (record, item) => {
           },
           },
         });
         });
       }
       }
-      // router.push({ path: "/sceneomore/obj/" + item.num });
       break;
       break;
     case "obj":
     case "obj":
       handleGenerate(item)
       handleGenerate(item)
@@ -165,7 +165,7 @@ const handleItem = async (record, item) => {
 };
 };
 const handleGenerate = async (item) => {
 const handleGenerate = async (item) => {
       const { id, num } = item
       const { id, num } = item
-      let { buildObjStatus } = await sceneDetail({num, isObj: 0});
+      let { buildObjStatus } = await sceneDetail({num, isObj: isObj.value});
       let toastText = buildObjStatus == 2 ? t('scene.4DMega.Modifying') : buildObjStatus == 1 ? t('scene.4DMega.coverData') : t('scene.4DMega.updateSuccess')
       let toastText = buildObjStatus == 2 ? t('scene.4DMega.Modifying') : buildObjStatus == 1 ? t('scene.4DMega.coverData') : t('scene.4DMega.updateSuccess')
       if (buildObjStatus !== 2) {
       if (buildObjStatus !== 2) {
         Modal.confirm({
         Modal.confirm({
@@ -218,7 +218,7 @@ const hendleCooperation = (item) => {
       Modal.confirm({
       Modal.confirm({
         title: t("common.tip"),
         title: t("common.tip"),
         icon: createVNode(ExclamationCircleFilled),
         icon: createVNode(ExclamationCircleFilled),
-        content: "计算中/已封存的场景无法协作",
+        content: t("scene.sceneAdmin.cooperationBlockedTip"),
         centered: true,
         centered: true,
         // okText: '确认',
         // okText: '确认',
         // cancelText: '取消',
         // cancelText: '取消',
@@ -325,11 +325,11 @@ onMounted(() => {
         <!-- 顶部搜索插槽 -->
         <!-- 顶部搜索插槽 -->
         <template #header>
         <template #header>
           <div
           <div
-            class="tableHeader flex justify-between items-center mb-5 w-full"
+            class="flex items-center justify-between w-full mb-5 tableHeader"
             v-if="selectTable.length == 0"
             v-if="selectTable.length == 0"
           >
           >
             <div
             <div
-              class="headerLeft flex justify-start items-center gap-x-4 position-relative"
+              class="flex items-center justify-start headerLeft gap-x-4 position-relative"
             >
             >
               <a-input-group compact style="width: auto; margin-right: 10px">
               <a-input-group compact style="width: auto; margin-right: 10px">
                 <a-input
                 <a-input
@@ -377,7 +377,7 @@ onMounted(() => {
                 />
                 />
               </a-space>
               </a-space>
             </div>
             </div>
-            <div class="headerRight flex items-center gap-4">
+            <div class="flex items-center gap-4 headerRight">
               <ul
               <ul
                 class="folder-path"
                 class="folder-path"
                 v-show="folderPath && folderPath.length > 1"
                 v-show="folderPath && folderPath.length > 1"
@@ -414,7 +414,7 @@ onMounted(() => {
             </div>
             </div>
           </div>
           </div>
           <div
           <div
-            class="tableHeader flex justify-end items-center mb-5 w-full"
+            class="flex items-center justify-end w-full mb-5 tableHeader"
             v-else
             v-else
           >
           >
             <a-space size="middle">
             <a-space size="middle">
@@ -444,37 +444,37 @@ onMounted(() => {
         <!-- 操作列插槽 -->
         <!-- 操作列插槽 -->
         <template #sceneName="{ record }">
         <template #sceneName="{ record }">
           <div
           <div
-            class="sceneName flex items-center cursor-pointer"
+            class="flex items-center cursor-pointer sceneName"
             @click="handleSee(record)"
             @click="handleSee(record)"
           >
           >
-            <div class="sceneleft w-32 h-22 relative rounded-lg shrink-0">
+            <div class="relative w-32 rounded-lg sceneleft h-22 shrink-0">
               <img
               <img
                 v-if="record.tbType == 'f'"
                 v-if="record.tbType == 'f'"
                 @click.stop="handleFolderItem(record)"
                 @click.stop="handleFolderItem(record)"
                 src="/src/assets/image/scene/folder.png"
                 src="/src/assets/image/scene/folder.png"
                 alt=""
                 alt=""
-                class="Folder w-32 h-22 mr-2"
+                class="w-32 mr-2 Folder h-22"
               />
               />
               <img
               <img
                 v-else
                 v-else
                 :src="getStatusImg(record)"
                 :src="getStatusImg(record)"
                 alt=""
                 alt=""
-                class="w-32 h-22 rounded-lg mr-2"
+                class="w-32 mr-2 rounded-lg h-22"
               />
               />
 
 
               <div
               <div
-                class="loading-hover absolute rounded-lg"
+                class="absolute rounded-lg loading-hover"
                 v-if="record.tbType != 'f' && record.status != -2"
                 v-if="record.tbType != 'f' && record.status != -2"
               >
               >
                 <span>{{ getStatusText(record) }}</span>
                 <span>{{ getStatusText(record) }}</span>
               </div>
               </div>
             </div>
             </div>
-            <div class="sceneNameTight ml-4">
-              <div class="sceneNameTitle text-base" style="color: #1d2129">
+            <div class="ml-4 sceneNameTight">
+              <div class="text-base sceneNameTitle" style="color: #1d2129">
                 {{ record.name || record.sceneName || record.title }}
                 {{ record.name || record.sceneName || record.title }}
               </div>
               </div>
               <div
               <div
-                class="sceneTime text-sm"
+                class="text-sm sceneTime"
                 v-if="record.tbType == 'f' && record.sceneNum"
                 v-if="record.tbType == 'f' && record.sceneNum"
                 style="margin-top: 6px; color: #86909c"
                 style="margin-top: 6px; color: #86909c"
               >
               >
@@ -483,10 +483,10 @@ onMounted(() => {
                 }}
                 }}
               </div>
               </div>
               <div
               <div
-                class="sceneTime text-sm"
+                class="text-sm sceneTime"
                 style="margin-top: 6px; color: #86909c"
                 style="margin-top: 6px; color: #86909c"
               >
               >
-                更新于{{ record.createTime }}
+                {{ t("scene.updatedAt", { time: record.createTime }) }}
               </div>
               </div>
             </div>
             </div>
           </div>
           </div>
@@ -526,13 +526,13 @@ onMounted(() => {
                 >
                 >
                   <a-menu-item style="width: 140px" key="copy">
                   <a-menu-item style="width: 140px" key="copy">
                     <div class="item">
                     <div class="item">
-                      <i class="iconfont icon-menu_copy"></i> 复制
+                      <i class="iconfont icon-menu_copy"></i> {{ t("scene.myScenes.share.copy") }}
                     </div>
                     </div>
                   </a-menu-item>
                   </a-menu-item>
                   <a-menu-item class="item" key="obj" v-if="(record.location != 5 && record.location != 6) && !record.isCopy">
                   <a-menu-item class="item" key="obj" v-if="(record.location != 5 && record.location != 6) && !record.isCopy">
                     <div class="item">
                     <div class="item">
                       <icon-font type="icon-scene_c" style="font-size: 16px" />
                       <icon-font type="icon-scene_c" style="font-size: 16px" />
-                      生成obj
+                      {{ t("scene.myScenes.produce") }}
                     </div>
                     </div>
                   </a-menu-item>
                   </a-menu-item>
                   <!-- <a-menu-item class="item" key="offline">
                   <!-- <a-menu-item class="item" key="offline">
@@ -547,7 +547,7 @@ onMounted(() => {
                   </a-menu-item> -->
                   </a-menu-item> -->
                   <a-menu-item class="item" key="more">
                   <a-menu-item class="item" key="more">
                     <div class="item">
                     <div class="item">
-                      <i class="iconfont icon-look_flatten"></i> 更多
+                      <i class="iconfont icon-look_flatten"></i> {{ t("scene.moreAction") }}
                     </div>
                     </div>
                   </a-menu-item>
                   </a-menu-item>
                 </a-menu>
                 </a-menu>
@@ -557,7 +557,7 @@ onMounted(() => {
         </template>
         </template>
         <!-- 协作插槽 -->
         <!-- 协作插槽 -->
         <template #cooperation="{ record }">
         <template #cooperation="{ record }">
-          <div class="cooperation cursor-pointer" @click="hendleCooperation(record)">
+          <div class="cursor-pointer cooperation" @click="hendleCooperation(record)">
             {{
             {{
               record.cooperationUserList && record.cooperationUserList.length || 0
               record.cooperationUserList && record.cooperationUserList.length || 0
             }}
             }}
@@ -567,7 +567,7 @@ onMounted(() => {
     </div>
     </div>
     <div class="moadl">
     <div class="moadl">
       <cooperation ref="cooperationRef" />
       <cooperation ref="cooperationRef" />
-      <uploadModal ref="uploadModalRef" @refresh="handleUploadSuccess" />
+      <uploadModal isObj="1" ref="uploadModalRef" @refresh="handleUploadSuccess" />
       <moveSceneModal ref="moveModalRef" @refresh="baseTableRef?.refresh" />
       <moveSceneModal ref="moveModalRef" @refresh="baseTableRef?.refresh" />
       <renameModal
       <renameModal
         v-model:visible="reName.show"
         v-model:visible="reName.show"