普通视图

发现新文章,点击刷新页面。
今天 — 2025年10月29日掘金 前端

Vue3项目中弹窗数据不回显问题

作者 灵魂出窍
2025年10月29日 11:22

需求:Vue3项目+Element Plus项目,点击table列表中“修改”按钮,弹窗回显该行数据。

问题:点击“修改”按钮,弹窗没有回显改行数据。

原代码展示

<template>
  <div class="app-container">
    <!-- 条件查询 -->
    <el-form :model="queryParams" ref="queryRef" v-show="showSearch" :inline="true" label-width="68px">
      <el-form-item label="Key" prop="key">
        <el-input v-model="queryParams.key" placeholder="请输入cookie/token的Key" clearable style="width: 240px"
          @keyup.enter="handleQuery" />
      </el-form-item>
      <el-form-item label="Name" prop="name">
        <el-input v-model="queryParams.name" placeholder="请输入Name" clearable style="width: 240px"
          @keyup.enter="handleQuery" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form> <!-- 表格数据 -->
    <el-table v-loading="loading" :data="cookiesList" @selection-change="handleSelectionChange" ref="multipleTable">
      <el-table-column type="selection" width="55"></el-table-column>
      <el-table-column label="ID" prop="id" width="80" align="center" />
      <el-table-column label="类型" prop="type" :show-overflow-tooltip="true" width="120" align="center" />
      <el-table-column label="Name" prop="name" :show-overflow-tooltip="true" width="140" align="center" />
      <el-table-column label="Key" prop="key" :show-overflow-tooltip="true" width="160" align="center" />
      <el-table-column label="Value" prop="value" :show-overflow-tooltip="true" min-width="200"> <template
          #default="scope"> <span class="value-text">{{ scope.row.value }}</span>
        </template>
      </el-table-column>
      <el-table-column label="创建时间" align="center" prop="create_time" width="200"> <template #default="scope">
          <span>{{ dayjs(scope.row.create_time).format('YYYY-MM-DD HH:mm:ss') }}</span>
        </template>
      </el-table-column> <!-- 操作列-修改 -->
      <el-table-column label="操作" align="center" width="120"> <template #default="scope">
          <el-button type="text" @click="handleEdit(scope.row)">修改</el-button>
        </template>
      </el-table-column>
    </el-table> <!-- 分页 -->
    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
      v-model:limit="queryParams.pageSize" @pagination="getList" /> <!-- 修改Value弹窗 -->
    <el-dialog v-model="editDialogVisible" title="修改Cookies Value" :width="500" @close="resetEditDialog">
      <el-form ref="editForm" :model="editForm" :rules="editRules" label-width="80px">
        <el-form-item label="ID" prop="id">
          <el-input v-model="editForm.id" disabled />
        </el-form-item>
        <el-form-item label="Key" prop="key">
          <el-input v-model="editForm.key" disabled />
        </el-form-item>
        <el-form-item label="Value" prop="value">
          <el-input v-model="editForm.value" type="textarea" :rows="6" placeholder="请输入修改后的Value" maxlength="10000"
            show-word-limit />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="resetEditDialog">取消</el-button>
        <el-button type="primary" @click="confirmEdit">确认修改</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script setup name="CookiesManagement">
  //引入自己的接口路径
import dayjs from 'dayjs';
import { ref, reactive, toRefs, getCurrentInstance } from 'vue';
const { proxy } = getCurrentInstance();
const cookiesList = ref([]);
const loading = ref(true);
const showSearch = ref(true);
const total = ref(0); // 用于存储选中的行数据 
const multipleSelection = ref([]); // 表格的ref,用于调用表格的方法 
const multipleTable = ref(null); // 查询参数 
const data = reactive({ queryParams: { pageNum: 1, pageSize: 10, key: undefined, name: undefined, }, });
const { queryParams } = toRefs(data); // 修改弹窗相关 
const editDialogVisible = ref(false);
const editForm = reactive({ id: '', key: '', value: '' });
const editRules = { value: [{ required: true, message: '请输入修改后的Value', trigger: 'blur' }, { max: 10000, message: 'Value不能超过10000个字符', trigger: 'blur' }] };
 /** 获取Cookies列表 */ function getList() {
    loading.value = true;
    const params = { ...queryParams.value };
    getCookiesList(params).then(response => { cookiesList.value = response.data.data; total.value = response.data.total; loading.value = false; }).catch(error => { loading.value = false; proxy.$message.error('获取Cookies列表失败,请稍后重试'); console.error('获取列表失败:', error); });
}
 /** 搜索按钮操作 */ 
 function handleQuery() { queryParams.value.pageNum = 1; getList(); }
  /** 重置按钮操作 */ 
  function resetQuery() { proxy.resetForm("queryRef"); handleQuery(); }
   /** 选择项变化时触发 */
    function handleSelectionChange(val) { multipleSelection.value = val; } 
    /** 打开修改弹窗 */
     function handleEdit(row) { 
     // 赋值当前行数据到修改表单 
     editForm.id = row.id; editForm.key = row.key; editForm.value = row.value; editDialogVisible.value = true; }
      /** 确认修改Value */ 
      function confirmEdit() { proxy.$refs.editForm.validate(async (valid) => { if (valid) { updateCookiesValue(editForm.id, editForm.value) .then(() => { proxy.$message.success('Value修改成功'); editDialogVisible.value = false; getList().catch(err => { proxy.$message.error('修改成功,但刷新列表失败'); console.error('列表刷新失败:', err); }); }) .catch((error) => { 
        // 捕获接口调用失败的错误 
        proxy.$message.error('修改时发生错误,请稍后重试'); console.error('修改失败:', error); }); } }); } 
        /** 重置修改弹窗 */ 
        function resetEditDialog() { editForm.id = ''; editForm.key = ''; editForm.value = ''; proxy.resetForm('editForm'); editDialogVisible.value = false; } 
        // 初始加载列表 
        getList(); 
</script>
<style scoped>
  .app-container {
    padding: 20px;
  }

  .value-text {
    color: #606266;
    word-break: break-all;
  }
</style>

问题原因: 获取当前实例的proxy方式在 Vue3 setup 中已失效,导致resetEditDialog函数执行时错误调用proxy.resetForm,清空了刚赋值的表单数据。

具体分析:在 Vue3 的<script setup>语法中,getCurrentInstance().proxy并非推荐且稳定的用法,尤其在最新版本的 Vue 或 Element Plus 中,该方式可能无法正确获取到组件实例,进而导致:

  1. resetEditDialog函数异常:每次打开弹窗前,若误触发resetEditDialog(如弹窗关闭时),会通过无效的proxy调用resetForm,但实际可能未正确重置表单,或在某些场景下反向清空已赋值的editForm
  2. 数据赋值被覆盖:即使handleEdit中给editForm赋了值,但后续若因proxy失效导致的表单重置逻辑异常,会覆盖已赋值的idkeyvalue,最终表现为弹窗无数据。

解决方案:

1.替换proxy,使用 Vue3 推荐的表单操作方式

<script setup>中,无需通过getCurrentInstance获取proxy,直接使用ref获取表单实例,调用resetFields方法(Element Plus 表单的标准重置方法)。

  1. 调整弹窗关闭时的重置逻辑 为避免关闭弹窗时立刻清空数据,可将resetEditDialog的清空逻辑延迟到弹窗打开前执行,而非关闭时

修改后的完整代码:

零依赖!教你用原生 JS 把 JSON 数组秒变 CSV 文件

作者 技术小丁
2025年10月29日 11:22

一、核心思路

  1. 把 JS 数组拼成「逗号分隔 + 换行」的字符串 → 这就是 CSV 的“文本协议”。
  2. 利用 Blob 把字符串变成文件流。
  3. 创建一个看不见的 <a> 标签,给它一个 download 属性,再自动点一下,浏览器就会弹出保存框。

二、核心代码

1. 准备原始数据

原始数据可以是接口返回,也可以是 mock。

const posts = [
  { id:1, title:'用 Vite 搭建 React 18 项目', link:'...', img:'...', views:12034 },
  // ...
];

2. 定义表头

顺序随意,只要和下面 map 对应即可。

const headers = ['id','名称','链接','图片','阅读'];

3. 拼接数据

const csvContent = [
  headers.join(','), // 第一行:表头
  ...posts.map(item => [ // 剩余行:数据
    `"${item.id}"`,
    `"${item.title}"`,
    `"${item.link}"`,
    `"${item.img}"`,
    `"${item.views}"`
  ].join(','))
].join('\n');

4. 生成文件并下载

const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.href = url;
link.download = `文章信息_${new Date().toISOString()}.csv`;
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);

三、完整代码

// 1. 造点假数据
const posts = [
  {
    id: 1,
    title: '用 Vite 搭建 React 18 项目',
    link: 'https://example.com/vite-react18',
    img: 'https://example.com/cover/vite-react.jpg',
    views: 12034
  },
  {
    id: 2,
    title: 'Tailwind CSS 3 响应式布局技巧',
    link: 'https://example.com/tailwind-layout',
    img: 'https://example.com/cover/tailwind.jpg',
    views: 8721
  },
  {
    id: 3,
    title: '深入浅出浏览器事件循环',
    link: 'https://example.com/event-loop',
    img: 'https://example.com/cover/event-loop.jpg',
    views: 15003
  },
  {
    id: 4,
    title: 'Webpack 5 性能优化清单',
    link: 'https://example.com/webpack5-optimize',
    img: 'https://example.com/cover/webpack.jpg',
    views: 9855
  },
  {
    id: 5,
    title: '前端图片懒加载完整方案',
    link: 'https://example.com/lazy-load',
    img: 'https://example.com/cover/lazy-load.jpg',
    views: 6542
  }
];

// 2. 组装 CSV
const headers = ['id', '名称', '链接', '图片', '阅读'];
const csvContent = [
  headers.join(','),
  ...posts.map(item => [
    `"${item.id}"`,
    `"${item.title}"`,
    `"${item.link}"`,
    `"${item.img}"`,
    `"${item.views}"`
  ].join(','))
].join('\n');

// 3. 下载
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `文章信息_${new Date().toISOString()}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);   // 释放内存

快速体验

  • 打开任意网页,F12 进控制台
  • 把完整代码全部粘进去,回车

关于uniapp开发小程序相册拖拽排序组件

作者 mozhijie6
2025年10月29日 11:15

组件代码片段

使用: 模板代码

<view class="drag-box">
    <AlbumDragSort :number="100" v-model="sortList" @onChoose="handleChoose" />
</view>

JS关键代码

 data() {
        return {
            imageList: [],
            sortList: []
        }
    },
    watch: {
        sortList: {
            handler(newVal, oldVal) {
                // console.log('sortList 变化了', newVal);
                this.imageList = newVal
            },
            deep: true
        },
        imageList: {
            handler(newVal, oldVal) {
                // console.log('imageList 变化了', newVal);
                this.sortList = newVal
            },
            deep: true
        }
    },
handleploadImage(images) {
    images.forEach((item, index) => {
        uploadImage({
            path: item.tempFilePath
        }).then(res => {
            if(res.errCode === 0) {
                this.imageList.push(res.data[0])
            }
        })
    })
},
handleChoose(tempFiles) {
    this.handleploadImage(tempFiles)
},

Vditor:markdown组件的使用

2025年10月29日 11:14

官网:b3log.org/vditor/

项目背景:做一个AI智能体的项目,sse接口返回markdown格式的流式数据,支持打字机效果,数学公式,流程图等等

效果:

纯预览:

image.png

可编辑:

image.png

组件代码:

<script setup lang="ts">
import {
  ref,
  watch,
  onMounted,
  onBeforeUnmount,
  computed,
  toRaw,
  nextTick
  // type PropType
} from 'vue'
import Vditor from 'vditor'
import 'vditor/dist/index.css'
import { merge } from 'lodash-es'

// eslint-disable-next-line turbo/no-undeclared-env-vars
const cdn = import.meta.env.DEV ? '/vditor' : '/console/vditor'
// const cdn = 'https://cdn.jsdelivr.net/npm/vditor@3.11.1'
// const cdn = 'http://10.0.102.120:9010/aig/vditor'
const defaultOptions = {
  mode: 'ir',
  height: 'auto',
  minHeight: 0,
  placeholder: '开始书写你的内容...',
  toolbarConfig: {
    pin: true
  },
  counter: {
    enable: true
  },
  cache: {
    enable: false
  },
  outline: {
    enable: true,
    position: 'right'
  }
}
const toolbarItems = ref([
  'emoji',
  'headings',
  'bold',
  'italic',
  'strike',
  'link',
  '|',
  'list',
  'ordered-list',
  'check',
  'outdent',
  'indent',
  '|',
  'quote',
  'line',
  'code',
  'inline-code',
  'insert-before',
  'insert-after',
  '|',
  'table',
  '|',
  'undo',
  'redo',
  '|',
  'fullscreen'
])

const props = defineProps({
  // 双向绑定值
  modelValue: {
    type: String,
    default: ''
  },
  // Vditor 配置选项
  options: {
    type: Object,
    default: () => ({})
  },
  type: {
    type: String,
    default: 'preview'
  },
  // 编辑器高度
  height: {
    type: [Number, String],
    default: 'auto'
  },
  // 是否启用上传功能
  enableUpload: {
    type: Boolean,
    default: true
  },
  // 禁用编辑器
  disabled: {
    type: Boolean,
    default: false
  }
})

const emit = defineEmits([
  'update:modelValue',
  'initialized',
  'rendered',
  'blur',
  'focus',
  'ready',
  'upload',
  'copy',
  'keydown',
  'destroyed'
])

const editorContainer = ref<HTMLElement | null>(null)
const vditorInstance = ref<Vditor | null>(null)
const isSettingValue = ref(false)
const isInitializing = ref(false)

// 处理容器高度
const containerStyles = computed(() => ({
  height: typeof props.height === 'number' ? `${props.height}px` : props.height
}))

// 清理容器内容
const cleanContainer = () => {
  if (editorContainer.value) {
    editorContainer.value.innerHTML = ''
    // 移除残留的Vditor相关类名(重要,因为公文写作显示预览模式,再是编辑模式 ,切换的时候会有残留的预览模式的类名,导致toolbar样式异常)
    editorContainer.value.className = 'vditor-editor-container'
  }
}

// 初始化编辑器
const initEditor = async () => {
  if (!editorContainer.value || isInitializing.value) return

  isInitializing.value = true

  try {
    // 先清理容器
    cleanContainer()

    // 纯预览处理
    if (props.type === 'preview') {
      await nextTick() // 确保容器清理完成
      await Vditor.preview(
        editorContainer.value as HTMLDivElement,
        props.modelValue,
        {
          mode: 'light',
          cdn
        }
      )
      return
    }

    // 销毁现有实例
    if (vditorInstance.value) {
      await destroyEditor()
      await nextTick() // 等待销毁完成
    }

    const mergedOptions: any = {
      ...merge(defaultOptions, toRaw(props.options)),
      input: handleInput,
      after: handleInitialized,
      focus: handleFocus,
      blur: handleBlur,
      keydown: handleKeyDown,
      value: props.modelValue,
      toolbar: toolbarItems.value,
      theme: props.options.theme || 'classic',
      preview: {
        ...(props.options.preview || {}),
        markdown: {
          sanitize: true,
          ...(props.options.preview?.markdown || {})
        }
      },
      cdn
    }

    if (props.enableUpload) {
      mergedOptions.upload = {
        accept: 'image/*,.zip,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.txt',
        multiple: true,
        ...(props.options.upload || {}),
        handler: handleUpload
      }
    }

    // 等待DOM完全准备好
    await nextTick()

    // 创建新实例
    vditorInstance.value = new Vditor(editorContainer.value, mergedOptions)
  } finally {
    isInitializing.value = false
  }
}

// 处理输入事件
const handleInput = (value: string) => {
  if (isSettingValue.value) {
    isSettingValue.value = false
    return
  }

  emit('update:modelValue', value)
  emit('rendered', vditorInstance.value)
}

// 处理初始化完成事件
const handleInitialized = () => {
  emit('initialized', vditorInstance.value)
  emit('ready', vditorInstance.value)

  // 监听复制事件
  if (vditorInstance.value) {
    vditorInstance.value.vditor?.element?.addEventListener('copy', handleCopy)
  }
}

// 处理上传
const handleUpload = (files: File[]) => {
  if (files.length === 0) return

  // 触发自定义上传事件
  emit('upload', {
    files,
    uploadCallback: (successURLs = []) => {
      if (!vditorInstance.value) return

      // 添加上传成功的图片/文件
      successURLs.forEach((url: string) => {
        if (
          url.endsWith('.jpg') ||
          url.endsWith('.png') ||
          url.endsWith('.gif')
        ) {
          vditorInstance.value?.insertValue(`![](${url})`)
        } else {
          vditorInstance.value?.insertValue(`[${url.split('/').pop()}](${url})`)
        }
      })
    }
  })
}

// 处理聚焦事件
const handleFocus = () => {
  emit('focus', vditorInstance.value)
}

// 处理失焦事件
const handleBlur = () => {
  emit('blur', vditorInstance.value)
}

// 处理按键事件
const handleKeyDown = (event: any) => {
  emit('keydown', {
    event,
    instance: vditorInstance.value
  })
}

// 处理复制事件
const handleCopy = (event: any) => {
  emit('copy', {
    event,
    instance: vditorInstance.value
  })
}

// 销毁编辑器
const destroyEditor = (): Promise<void> => {
  return new Promise((resolve) => {
    if (
      vditorInstance.value &&
      vditorInstance.value.vditor &&
      vditorInstance.value.vditor.element
    ) {
      // 移除事件监听器
      vditorInstance.value.vditor.element.removeEventListener(
        'copy',
        handleCopy
      )
      // 销毁Vditor实例
      vditorInstance.value.destroy()
      vditorInstance.value = null
      emit('destroyed')

      // 使用requestAnimationFrame确保DOM更新完成
      requestAnimationFrame(() => {
        resolve()
      })
    } else if (vditorInstance.value) {
      // 如果 vditorInstance.value 存在但 vditor 或 element 不存在,只清空实例
      vditorInstance.value = null
      emit('destroyed')
      resolve()
    } else {
      resolve()
    }
  })
}
// 暴露编辑器实例方法
const getVditorInstance = () => vditorInstance.value

// 设置编辑器内容
const setValue = (value: string, clearStack = true) => {
  if (vditorInstance.value) {
    isSettingValue.value = true
    vditorInstance.value.setValue(value, clearStack)
  }
}

// 清除内容
const clearContent = () => {
  setValue('')
}

// 获取编辑器内容
const getValue = () => {
  return vditorInstance.value?.getValue() || ''
}

// 聚焦编辑器
const focusEditor = () => {
  vditorInstance.value?.focus()
}

// 失焦编辑器
const blurEditor = () => {
  vditorInstance.value?.blur()
}

// 禁用/启用编辑器
const toggleDisabled = (disabled: boolean) => {
  if (vditorInstance.value) {
    if (disabled) {
      vditorInstance.value.disabled()
    } else {
      vditorInstance.value.enable()
    }
  }
}

// 监听模型值变化
watch(
  () => props.modelValue,
  async (newValue) => {
    console.log(newValue)
    // 纯预览处理
    if (props.type === 'preview') {
      if (editorContainer.value) {
        // editorContainer.value.innerHTML = await Vditor.md2html(newValue, {
        //   mode: 'light',
        //   cdn
        // })
        await Vditor.preview(
          editorContainer.value as HTMLDivElement,
          props.modelValue,
          {
            mode: 'light',
            cdn
          }
        )
      }
      // Vditor.mathRender(editorContainer.value as HTMLElement, {
      //   cdn,
      //   math: {
      //     engine: 'KaTeX',
      //     inlineDigit: true
      //   }
      // })
      return
    }
    const currentValue = getValue()

    // 只有当内容确实改变时才更新编辑器
    if (newValue !== currentValue) {
      setValue(newValue)
    }
  }
)

// 监听禁用状态变化
watch(
  () => props.disabled,
  (disabled) => {
    toggleDisabled(disabled)
  }
)

// 监听选项变化
watch(
  () => props.options,
  () => {
    void initEditor()
  },
  { deep: true }
)

// 监听工具栏变化
// watch(
//   () => props.toolbarItems,
//   () => {
//     void initEditor()
//   }
// )

//监听type变化
watch(
  () => props.type,
  async (newType, oldType) => {
    // 避免重复初始化
    if (newType === oldType || isInitializing.value) return

    // 重新初始化编辑器
    await initEditor()
  }
)

// 生命周期钩子
onMounted(initEditor)
onBeforeUnmount(destroyEditor)

// 暴露公共方法
defineExpose({
  getVditorInstance,
  getValue,
  setValue,
  focusEditor,
  blurEditor,
  clearContent,
  insertValue: (value: string) => vditorInstance.value?.insertValue(value),
  getCursorPosition: () => vditorInstance.value?.getCursorPosition() || 0,
  enable: () => vditorInstance.value?.enable(),
  disabled: () => vditorInstance.value?.disabled(),
  destroy: destroyEditor
})
</script>
<template>
  <div
    ref="editorContainer"
    :style="containerStyles"
    class="vditor-editor-container"
  />
</template>
<style lang="postcss">
.vditor-editor-container {
  img {
    width: 70%;
    display: block;
    margin-top: 20px;
  }
  border: none;
  table {
    width: 100%;
    tr {
      th,
      td {
        text-align: left;
        padding: 8px 12px;
        border: 1px solid #f0f0f0;
      }
    }
  }
  /* 格式化内容样式 */
  h1 {
    line-height: max(1.5em, 32px); /* 现代浏览器动态适配 */
  }
  h2 {
    font-size: 18px;
    font-weight: 700;
    margin: 20px 0 12px;
    line-height: 1.4;
  }
  /* 标题样式 - 只使用h3 */
  h1 {
    font-size: 24px;
    font-weight: 700;
    margin: 20px 0 12px;
    line-height: 1.4;
  }
  h3 {
    font-size: 16px;
    font-weight: 700;
    margin: 20px 0 12px;
    line-height: 1.4;
  }
  h1:first-child {
    margin-top: 0;
  }

  h3:first-child {
    margin-top: 0;
  }

  /* 副标题样式 - 使用strong */

  strong {
    font-weight: 700;
    color: #1d2129;
  }

  p {
    line-height: 1.8;
  }

  /* 确保段落之间有足够间距 */
  p + p {
    margin-top: 16px;
  }

  ul,
  ol {
    padding-left: 1.8rem;
    margin: 14px 0 14px;
  }

  ul {
    list-style-type: disc;
  }

  ol {
    list-style-type: decimal;
  }

  li {
    margin: 10px 0;
    padding-left: 0.3rem;
    line-height: 1.6;
  }

  li:last-child {
    margin-bottom: 10px;
  }

  strong {
    font-weight: 600;
  }

  /* 分隔线样式 - 更浅的颜色,更大的间距 */
  hr {
    border: 0;
    height: 1px;
    background-color: #e1dede !important;
    margin: 14px 0;
  }

  pre {
    code {
      white-space: pre-wrap;
    }
  }
}
</style>

怎么使用:

<AiMarkdown
        class="text-[#1D2129] break-all"
        :model-value="message.content"
        type="preview"
      />

5. view component

2025年10月29日 11:06
  1. 大脑(State) :负责存储和管理文档的内容、光标位置等信息。这就是 Editor State
  2. 脸和手脚(View) :负责把文档显示给你看,并且接收你的操作(打字、点击等)。这就是 Editor View

5.1 Editable DOM

ProseMirror 最终需要将它的文档模型渲染成浏览器能理解的 HTML(也就是 DOM)。它默认会使用你在 Schema 中定义的 toDOM 方法来生成这个 DOM 结构,并给这个 DOM 元素加上 contenteditable="true" 属性,使其变成可编辑区域。

5.1 Data flow

数据流描述的是信息在系统中如何流动和变化。在 ProseMirror 中,它特指状态(State)、视图(View)和事务(Transaction) 三者之间如何相互作用。

  1. View 显示 State:视图负责将当前的状态(文档内容、光标位置)渲染到页面上。
  2. 用户触发事件:你在视图上进行了操作(如打字、点击)。
  3. View 产生 Transaction:视图将这个操作“翻译”成一个 Transaction(事务)。Transaction 描述了“发生了什么变化”。
  4. State 应用 Transaction:这个 Transaction 被传递给当前的状态,状态根据 Transaction 的指示计算出一个新的状态
  5. 新 State 更新 View:新的状态被设置回视图 (view.updateState(newState)),视图根据新状态重新渲染,更新显示。

第1步:定义应用总状态

let appState = {
  editor: EditorState.create({schema}), // ProseMirror的编辑器状态
  score: 0 // 应用自己的状态,比如一个游戏分数
}

第2步:创建视图并“交出控制权”

let view = new EditorView(document.body, {
  state: appState.editor, // 初始状态来自appState
  dispatchTransaction(transaction) { // 关键!拦截事务
    update({type: "EDITOR_TRANSACTION", transaction}) // 交给全局更新函数
  }
})

第3步:创建中央更新函数

function update(event) {
  if (event.type == "EDITOR_TRANSACTION")
    // 只有这里才能更新编辑器状态!
    appState.editor = appState.editor.apply(event.transaction)
  else if (event.type == "SCORE_POINT")
    // 也可以处理其他类型的更新,比如加分
    appState.score++
  draw() // 状态更新后,统一刷新UI
}

第4步:创建UI刷新函数

function draw() {
  // 更新分数显示
  document.querySelector("#score").textContent = appState.score
  // 更新编辑器视图!这是连接回ProseMirror的关键一步
  view.updateState(appState.editor)
}

5.2 Decorations

不改变文档内容,只改变“视图的渲染样式/DOM”。适合做高亮、下划线、提示气泡、占位符等。

  • 三种类型
  • Node 装饰:给某个节点的 DOM 添属性/样式(如给所有段落加 class)。
  • Inline 装饰:给一段范围内的“行内内容”加样式(如高亮关键词)。
  • Widget 装饰:在某个文档位置插入一个独立的 DOM 节点(不属于文档),比如光标提示、占位符、按钮。
import { Plugin } from 'prosemirror-state'
import { Decoration, DecorationSet } from 'prosemirror-view'

const purplePlugin = new Plugin({
  props: {
    decorations(state) {
      return DecorationSet.create(state.doc, [
        Decoration.inline(0, state.doc.content.size, { style: 'color: purple' })
      ])
    }
  }
})

每次重绘都“重新创建”,当装饰很多时会变慢。

正确做法(维护在插件 state 中 + 映射)

  • 把 DecorationSet 存在插件的 state 里。
  • 在 apply(tr, set) 中用 set.map(tr.mapping, tr.doc) 把装饰映射到新文档(文本插入/删除后仍能“跟着走”)。
  • 只有需要时才“增删修改”装饰,避免每帧重建。
import { Plugin } from 'prosemirror-state'
import { Decoration, DecorationSet } from 'prosemirror-view'

export const specklePlugin = new Plugin<DecorationSet>({
  state: {
    init(_, { doc }) {
      const speckles: Decoration[] = []
      for (let pos = 1; pos < doc.content.size; pos += 4) {
        speckles.push(
          Decoration.inline(pos - 1, pos, { style: 'background: yellow' })
        )
      }
      return DecorationSet.create(doc, speckles)
    },
    apply(tr, set) {
      // 让装饰“映射”到新文档结构(跟随插入/删除移动)
      const mapped = set.map(tr.mapping, tr.doc)
      // 这里还可以基于 tr 添加/移除装饰(如响应搜索结果、校验结果等)
      return mapped
    }
  },
  props: {
    decorations(state) {
      return specklePlugin.getState(state)
    }
  }
})

三类装饰直观例子

  1. Node 装饰:给所有段落加类名
const paragraphClassPlugin = new Plugin({
  props: {
    decorations(state) {
      const decos: Decoration[] = []
      state.doc.descendants((node, pos) => {
        if (node.type.name === 'paragraph') {
          decos.push(
            Decoration.node(pos, pos + node.nodeSize, { class: 'pm-paragraph' })
          )
        }
      })
      return DecorationSet.create(state.doc, decos)
    }
  }
})
  1. Inline 装饰:高亮关键字(简单搜索)
// 例子:高亮选中文本
const highlightDecoration = Decoration.inline(
  10,  // 开始位置
  20,  // 结束位置
  {
    style: 'background-color: yellow; padding: 2px;',
    class: 'highlight'
  },
  {
    inclusiveStart: true,  // 包含起始位置的新内容
    inclusiveEnd: false    // 不包含结束位置的新内容
  }
)
  1. 装饰不影响文档内容 - 只是视觉表现

  2. 三种装饰类型

    • Widget: 插入自定义DOM元素
    • Inline: 给文本范围添加样式
    • Node: 给整个节点添加样式

5.3 Node views

为文档中的特定节点类型创建"自定义 UI 组件"。就像 React 组件一样,你可以完全控制某个节点的渲染、更新和交互

  • 核心概念
  • dom:节点的 DOM 表示
  • contentDOM:内容渲染区域(可选)
  • update():节点更新时的处理逻辑
  • stopEvent():是否阻止事件冒泡
  1. 自定义图片节点
import { EditorView } from 'prosemirror-view'
import { Node as ProseMirrorNode } from 'prosemirror-model'

class ImageView {
  dom: HTMLImageElement
  node: ProseMirrorNode
  view: EditorView
  getPos: () => number
  
  
   constructor(node: ProseMirrorNode, view: EditorView, getPos: () => number) {
    this.node = node
    this.view = view
    this.getPos = getPos

    // 创建自定义 DOM
    this.dom = document.createElement('img')
    this.dom.src = node.attrs.src
    this.dom.alt = node.attrs.alt || ''
    this.dom.style.maxWidth = '100%'
    this.dom.style.height = 'auto'

    // 添加点击事件
    this.dom.addEventListener('click', (e) => {
      e.preventDefault()
      console.log('图片被点击了!')
      this.handleImageClick()
    })
  }
  
  // 阻止事件冒泡,让编辑器忽略这个节点的事件
  stopEvent() {
    return true
  }

  // 处理图片点击:修改 alt 文本
  private handleImageClick() {
    const newAlt = prompt('请输入图片描述:', this.node.attrs.alt || '')
    if (newAlt !== null) {
      // 使用 getPos() 获取当前位置,然后更新节点属性
      const tr = this.view.state.tr.setNodeMarkup(this.getPos(), null, {
        ...this.node.attrs,
        alt: newAlt
      })
      this.view.dispatch(tr)
    }
  }
}

// 在 EditorView 中注册
const view = new EditorView(document.querySelector('#editor'), {
  state,
  nodeViews: {
    image(node, view, getPos) {
      return new ImageView(node, view, getPos)
    }
  }
})
  • 图片:点击编辑、拖拽调整大小
  • 表格:行/列操作、样式调整
  • 代码块:语法高亮、复制按钮
  • 视频:播放控制、字幕编辑
  • 数学公式:LaTeX 渲染、编辑界面

5.4. commands

  • 核心概念:Command 是一个函数,实现编辑操作(如删除、格式化、插入等)。用户通过快捷键或菜单触发。

  • 接口设计:(state, dispatch?, view?) => boolean

    • state:当前编辑器状态
    • dispatch:可选,用于执行事务
    • view:可选,编辑器视图实例
    • 返回值:true 表示命令执行成功,false 表示不适用

eg

import { EditorState, Transaction } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view'

****
function deleteSelection(state: EditorState, dispatch?: (tr: Transaction) => void): boolean {
  // 如果没有选中内容,命令不适用
  if (state.selection.empty) return false
  
  // 如果有 dispatch 函数,执行删除操作
  if (dispatch) {
    dispatch(state.tr.deleteSelection())
  }
  
  return true // 命令执行成功
}

// 使用示例
const view = new EditorView(document.querySelector('#editor'), { state })

// 检查命令是否可用(不执行)
const canDelete = deleteSelection(view.state, null) // 返回 true/false

// 实际执行命令
deleteSelection(view.state, view.dispatch) // 删除选中内容

原生 Vue + UniApp 的小程序或 App 项目里如何判断用户是否为首次下载应用

作者 Crystal328
2025年10月29日 10:43

一:本地存储标记法(常用)

核心思路:
uni.setStorageSync 存一个布尔值标志,判断是否第一次进入。

Vue2 写法

// App.vue
export default {
  onLaunch() {
    const firstEnter = uni.getStorageSync('firstEnter')
    if (!firstEnter) {
      console.log('✨ 首次进入应用')
      uni.setStorageSync('firstEnter', true)
      // 比如跳转到引导页
      uni.reLaunch({ url: '/pages/guide/guide' })
    } else {
      console.log('🚀 已经进入过')
    }
  }
}

Vue3 写法

// App.vue
import { onLaunch } from '@dcloudio/uni-app'

export default {
  setup() {
    onLaunch(() => {
      const hasEntered = uni.getStorageSync('hasEntered')
      if (!hasEntered) {
        console.log('✨ 首次进入')
        uni.setStorageSync('hasEntered', true)
        uni.reLaunch({ url: '/pages/guide/guide' })
      } else {
        console.log('🚀 非首次进入')
      }
    })
  }
}

优点:

  • 实现简单、逻辑清晰;
  • 无需网络请求;
  • 通用于小程序、App、H5。

缺点:

  • 卸载或清除缓存后会重新判断为“首次进入”;
  • 无法区分同一账号在不同设备的首次进入(仅限本设备)。

适用场景:
大部分项目的启动引导页、功能介绍页。

二:App 端文件标记(数据在设备中能保存更久,不容易被清除或覆盖。)

核心思路:
通过 plus.io(仅 App 端可用)在本地文件系统中保存标志文件,能防止被清除缓存影响。

vue2写法

// App.vue
export default {
  onLaunch() {
    // 仅在 App 端有效
    #ifdef APP-PLUS
    console.log('App 启动,检测是否首次进入...')
    plus.io.resolveLocalFileSystemURL('_doc/', (entry) => {
      // 尝试获取 firstEnter.txt 文件(create:false 表示不自动创建)
      entry.getFile(
        'firstEnter.txt',
        { create: false },
        (fileEntry) => {
          // 文件存在 → 非首次进入
          console.log('🚀 已存在文件 -> 非首次进入')
          // 你可以在这里执行“老用户逻辑”
        },
        (error) => {
          // 文件不存在 → 首次进入
          console.log('✨ 文件不存在 -> 首次进入')
          // 创建文件以记录状态
          entry.getFile('firstEnter.txt', { create: true }, (newFile) => {
            console.log('✅ 已创建标记文件,记录首次进入')
            // 执行首次进入逻辑,比如引导页
            uni.reLaunch({ url: '/pages/guide/guide' })
          })
        }
      )
    }, (err) => {
      console.error('❌ resolveLocalFileSystemURL 解析失败:', err)
    })
    #endif
  }
}


vue3写法

// App.vue
import { onLaunch } from '@dcloudio/uni-app'

export default {
  setup() {
    onLaunch(() => {
      // 仅在 App 端有效
      #ifdef APP-PLUS
      console.log('App 启动,检测是否首次进入...')
      plus.io.resolveLocalFileSystemURL(
        '_doc/',
        (entry) => {
          entry.getFile(
            'firstEnter.txt',
            { create: false },
            (fileEntry) => {
              console.log('🚀 已存在文件 -> 非首次进入')
              // 老用户逻辑放这里
            },
            (error) => {
              console.log('✨ 文件不存在 -> 首次进入')
              entry.getFile('firstEnter.txt', { create: true }, (newFile) => {
                console.log('✅ 已创建标记文件,记录首次进入')
                // 首次进入逻辑,例如跳转引导页
                uni.reLaunch({ url: '/pages/guide/guide' })
              })
            }
          )
        },
        (err) => {
          console.error('❌ resolveLocalFileSystemURL 解析失败:', err)
        }
      )
      #endif
    })
  }
}

注释

  • _doc/ 是 App 本地的持久化文件目录;
  • resolveLocalFileSystemURL 是解析逻辑路径的入口;
  • entry 是文件或目录对象,可以用来创建、读取、删除文件;
  • 这些都是 HTML5+ (plus.io) 提供的 API,仅在 App 端(APP-PLUS) 可用

常见路径

目录 说明 可写 是否清除
_doc/ App 数据存储目录(推荐) ×(仅卸载清除)
_downloads/ 下载文件保存目录 ×
_www/ 打包资源目录(App 安装包内) × ×
_documents/ 公开文档目录(用户可见) × ×
_temp/ 临时目录 √(系统可能清理)

_doc/ 是什么?

_doc/App 内部可读写的私有目录
可以理解为 App 的「本地数据区」。
类似于 Android 的 /data/data/包名/doc/ 或 iOS 的 Documents/ 文件夹。

在这个目录中保存的内容:

  • 不会被 uni.clearStorage() 清除;
  • 只有卸载 App 时才会被系统清理;
  • 可以安全地读写文件,比如 .txt.json、图片、日志等。

所以我们说它属于「高持久存储区」。


plus.io.resolveLocalFileSystemURL() 是什么?

这是 HTML5+ 提供的一个 路径解析函数
它的作用是把 _doc/_www/、本地文件路径等「逻辑路径」解析为一个可以操作的 文件系统入口对象(entry)

语法:

plus.io.resolveLocalFileSystemURL(path, successCallback, errorCallback)

示例:

plus.io.resolveLocalFileSystemURL('_doc/', (entry) => {
  console.log('成功解析到目录对象:', entry)
}, (err) => {
  console.error('解析失败:', err)
})

解析成功后,entry 就是一个目录对象或文件对象,可以用它来读、写、创建文件


entry 是什么?

entry 是解析后的文件系统入口对象,可以是文件或目录。
常用方法如下:

方法 作用
entry.getFile(filename, options, success, fail) 在目录下获取(或创建)文件
entry.remove(success, fail) 删除文件或目录
entry.moveTo() / copyTo() 移动或复制文件

优点:

  • 即使清除缓存,文件仍在;
  • 适合需要长期判断首次进入的 App。
    缺点:
  • 仅适用于 App,不支持小程序;
  • 代码稍复杂;
  • 不适合纯前端(H5)运行。

适用场景:
需要高持久性首次检测(比如 App 引导页、注册流程控制)。

三:基于用户账号(后端标记)

核心思路:
当用户登录后,通过服务器接口判断该用户是否第一次使用该账号登录

// login.vue
uni.login({
  success: async (res) => {
    const userInfo = await uni.request({
      url: 'https://api.xxx.com/checkFirstEnter',
      method: 'POST',
      data: { userId: res.userInfo.id }
    })
    if (userInfo.data.isFirstEnter) {
      console.log('✨ 该账号首次登录')
    } else {
      console.log('🚀 老用户')
    }
  }
})

优点:

  • 跨设备一致;
  • 适合有账号体系的应用;
  • 可统计新用户注册、留存。

缺点:

  • 依赖网络;
  • 需要后端配合;
  • 无法在未登录状态下判断。

适用场景:
需要区分新老用户的业务场景(如营销活动、统计分析)。

鸿蒙端云一体化云存储实战:手把手教你玩转文件上传下载

2025年10月29日 10:31

作为HarmonyOS 5.0.0(12)版本推出的云存储模块,可谓是真的把端云协同的丝滑体验拉满了!今天掌门人就带兄弟们从初始化到文件管理全流程拆解,让你看完就能在项目中落地实操。

一、初始化云存储实例:这步千万别跳过!

要玩转云存储,首先得初始化你的存储桶(StorageBucket)。官方文档里那个bucket()方法就是你的通行证:

import { cloudStorage } from '@kit.CloudFoundationKit'

// 使用默认实例(AGC后台配置好的)
let defaultBucket = cloudStorage.bucket()

// 或者指定自定义存储桶(命名规则:小写字母+数字+短横线,且不能连续两个短横线)
let customBucket = cloudStorage.bucket('mybucket-duaf5')

注意事项

  • 默认实例会自动查询AGC配置,但首次使用建议在AGC控制台确认好存储实例状态
  • 自定义实例名必须符合命名规范,否则会报401参数错误
  • 初始化操作必须在Stage模型下执行,元服务也支持啦(从5.0.0开始)

二、上传文件:从相册到云端的完整链路

上传操作是云存储的核心功能,uploadFile()方法让你轻松搞定:

// 示例:上传图片到云存储
bucket.uploadFile(context, {
  localPath: `${context.cacheDir}/photo.jpg`, // 本地路径(必须在cache目录下)
  cloudPath: `user_photos/${userId}.jpg`,     // 云端路径(支持目录结构)
  metadata: {                                  // 可选元数据
    contentType: 'image/jpeg',
    customMetadata: { 
      uploadBy: 'mobile'
    }
  },
  mode: request.agent.Mode.BACKGROUND,         // 任务类型(默认后台任务)
  network: request.agent.Network.WIFI          // 网络策略(这里限制仅WiFi)
})

进阶技巧

  • 监听上传进度:task.on('progress', (progress) => {...})
  • 错误处理:特别注意1008220001网络错误和1008221001服务端错误
  • 文件重命名:建议在cloudPath里加入时间戳防重名

三、下载文件:原生鸿蒙的丝滑体验

下载功能支持直接读取云端文件,不用再手动下载到本地:

bucket.downloadFile(context, {
  cloudPath: `user_photos/${userId}.jpg`,
  localPath: `${context.cacheDir}/download.jpg`,
  overwrite: true, // 是否覆盖已存在的文件
  mode: request.agent.Mode.FOREGROUND // 前台任务保证下载不中断
})

黑科技提示

  • 结合原生鸿蒙云空间的"自动释放本地存储"功能,云端视频/图片可直接在第三方APP调用
  • 使用getDownloadURL()获取永久下载链接,适合分享场景

四、文件管理:增删查改全套解决方案

1. 获取文件列表

// 获取根目录文件
bucket.list('', { maxResults: 20 }) 

// 获取指定目录下的文件
bucket.list('backup_logs/', { pageMarker: 'next_page_token' })

2. 删除文件

bucket.deleteFile(`temp_files/${fileName}`)

3. 元数据操作

// 获取元数据
bucket.getMetadata(`user_photos/${userId}.jpg`)

// 修改元数据
bucket.setMetadata(`user_photos/${userId}.jpg`, {
  cacheControl: 'public,max-age=3600'
})

五、那些年踩过的坑

  1. 权限配置:别忘了在config.json里添加ohos.permission.INTERNET
  2. 路径规范
    • 本地路径必须以cacheDir为根目录
    • 云端路径建议带业务前缀(如images/
  3. 免费额度:云存储默认5G免费空间,大文件建议做压缩处理
  4. 异步回调:Promise和callback两种方式任选,但要注意错误码处理

总结

兄弟们,掌握了这波云存储操作,你已经能在鸿蒙生态里叱咤风云了!从初始化到文件管理,每个环节都踩着HarmonyOS的节奏走,配合AGC的免费配额,开发效率直接起飞。记得在项目初期就规划好云端存储结构,后期维护才能游刃有余。

最后送大家一句话:端云一体化不是噱头,而是未来开发的必经之路

兄弟们赶紧去试试吧,有问题欢迎评论区交流~

图吧工具箱-电脑硬件圈的“瑞士军刀”

作者 非凡ghost
2025年10月29日 10:20

软件定位

  • 全称:图拉丁吧硬件检测工具箱(民间简称“图吧工具箱”)
  • 价格:永久免费,无广告,不开会员
  • 体积:185 MB 左右(Win 版),支持 Windows 7/8/10/11 64 位
  • 内核:收录 CPU-Z、GPU-Z、AIDA64、HWiNFO、FurMark、Prime95、DisplayX 等官方最新版,统一图形菜单,一键调用,无需重复安装

图片

亮点

  1. 全面支持 Intel 13/14 代酷睿 与 NVIDIA RTX 40/50 系显卡 检测
  2. 新增 DDR5 内存、PCIe 5.0 SSD 信息识别
  3. 显存温度压力测试回归,一键烤卡不再闪退 
  4. 工具库持续更新:CPU-Z、GPU-Z、HWiNFO、FurMark2 全部升至最新版
  5. 适配 Windows 12 预览版,高分辨率界面自动缩放

图片

功能分区

硬件信息 CPU-Z / GPU-Z / HWiNFO 处理器、主板、内存、显卡详细参数一次看清
性能测试 FurMark、Prime95、3DMark Demo 显卡烤机、CPU 压力、温度/功耗/频率曲线实时监控
外设检测 DisplayX、Keyboard Test 屏幕坏点、色域、键盘按键触发测试
硬盘工具 CrystalDiskInfo、AS SSD 通电时间、健康度、PCIe 速率、4K 读写跑分
综合检测 AIDA64 工程版 一键生成 40 页硬件报告,买二手电脑防翻车
系统维护 DDU 驱动卸载、Everything 搜索 清旧驱动、秒搜文件,重装系统必备

图片
图片

实用场景

  • 买新机/二手:先跑 HWiNFO + CrystalDiskInfo,看通电时长、电池循环、显卡核心/显存温度,避免矿卡翻新
  • 装系统后:DDU 清旧驱动 → FurMark 20 分钟烤卡 → Prime95 30 分钟烤 CPU,确认散热压得住
  • 升级前规划:主板型号、电源瓦数、PCIe 插槽版本一目了然,防止“i7 配 B660”尴尬

图吧工具箱 = 硬件圈的“瑞士军刀” :买机、验机、烤机、清驱动、查坏点、跑分、写报告,一个安装包全搞定免费、无广告、持续更新,电脑装机必备!

「图吧工具箱2025.07R2安装包.exe」 链接:pan.quark.cn/s/5c68cbac6…

飞书多维表格插件:进一步封装,提升开发效率!🚀

作者 橙某人
2025年10月29日 10:17

写在开头

Hello,各位好呀!今是 2025 年 10 月 26 日。😀

距离上次写文已经是一个多月前了呢,事有点多😋,简洁罗列记录下:

  • 又去爬了几次山,这次不仅广州,也去了周边城市
  • 参加了两次 TRAE 的线下活动
  • 换季中招,感冒了一场
  • 换了些身上的电子设备
  • ...

这一个多月过得还是比较充实的,平衡了工作与生活。💯

然后呢,最近小编在做飞书多维表格插件方面的业务开发,飞书官方提供了 JS SDK 来帮助咱们开发插件:

📖 官方文档:传送门

不过呢,官方 SDK 的方法都比较"原子化",虽然灵活,但写业务时往往需要把多个 API 组合起来才能完成一次具体操作。每次都要写一堆重复代码,有点...痛苦!😭

所以呢,小编基于项目里的真实需求,进一步做了"组合封装",让咱们在写插件时能"一把梭",效率蹭蹭往上涨!希望你能多一些时间出来摸鱼,沸点池里都没鱼了喂 (๑•̀ㅂ•́)و✧。

接下来,是一些方法情况的详情介绍,请诸君按需食用哈。

方法详解 🔧

在实际开发中,咱们经常需要:

  • 🔍 获取表格实例
  • 🏷️ 获取字段 ID
  • 📝 批量添加记录
  • ✅ 确保字段存在
  • 📊 处理大量数据的分批操作
  • 🔄 数据格式转换和映射
  • ...

基于这些常见需求,小编封装了一系列实用方法,让开发更加高效,告别重复造轮子!🎉

本次的封装形式是直接写了一个 bitable.js 的工具文件,使用ESM形式,使用非常简单,需要就直接导出使用即可。

1️⃣ 获取表格实例

功能说明:支持通过表格名称或ID获取表格实例,传入名称时若表格不存在会自动创建。

// 导入飞书多维表格的 JS SDK
import { bitable } from '@lark-base-open/js-sdk';

// 获取基础操作对象和UI操作对象
const base = bitable.base;
const ui = bitable.ui;

/**
 * 获取表格实例
 * @description 支持通过表格名称或ID获取表格实例,传入名称时若表格不存在会自动创建
 * @param {string} tableIdentifier - 表格标识符,可以是表格名称或表格ID
 * @returns {Promise<Object>} 返回表格实例对象
 * @example
 * // 通过表格名称获取(不存在则自动创建)
 * const table = await getTableInstance('我的数据表');
 * 
 * // 通过表格ID获取
 * const table = await getTableInstance('tblxxxxxxxxxxxxxx');
 */
export async function getTableInstance(tableIdentifier) {
  let targetTableId = ""; // 用于存储目标表格的ID

  // 判断传入的参数是表格ID还是表格名称
  // 表格ID通常以'tbl'开头且长度较长,或者长度超过15个字符
  const isTableId = tableIdentifier.length > 10 && (
    tableIdentifier.startsWith('tbl') || 
    tableIdentifier.length > 15
  );

  if (isTableId) {
    // 如果传入的是表格ID,直接通过ID获取表格实例
    const table = await base.getTable(tableIdentifier);
    return table; // 返回表格实例
  } else {
    // 如果传入的是表格名称,需要先尝试获取,失败则创建
    try {
      // 尝试通过名称获取已存在的表格
      const { id } = await base.getTable(tableIdentifier);
      targetTableId = id; // 获取成功,记录表格ID
    } catch (error) {
      // 表格不存在,创建新表格
      const { tableId } = await base.addTable({ name: tableIdentifier });
      targetTableId = tableId; // 记录新创建的表格ID
      await ui.switchToTable(targetTableId); // 自动切换到新创建的表格
    }
    // 通过表格ID获取表格实例并返回
    const table = await base.getTable(targetTableId);
    return table;
  }
}

使用示例:

// 通过“名称”获取(不存在则自动创建)
const table = await getTableInstance('分镜设计表');

// 或通过“ID”获取
const tableById = await getTableInstance('tblxxxxxxxxxxxxxx');

2️⃣ 获取字段ID

功能说明:通过字段名获取字段ID,若字段不存在则自动创建。

import { FieldType } from '@lark-base-open/js-sdk';

/**
 * 获取字段ID
 * @description 通过字段名获取字段ID,若字段不存在则自动创建
 * @param {Object} tableInstance - 表格实例对象
 * @param {string} fieldName - 字段名称
 * @param {FieldType} [fieldType=FieldType.Text] - 字段类型,默认为文本类型
 * @returns {Promise<string>} 返回字段ID
 * @example
 * // 获取文本字段ID(不存在则创建)
 * const fieldId = await getFieldId(tableInstance, '姓名');
 * 
 * // 获取数字字段ID
 * const numberFieldId = await getFieldId(tableInstance, '年龄', FieldType.Number);
 */
export async function getFieldId(tableInstance, fieldName, fieldType = FieldType.Text) {
  try {
    // 尝试获取已存在的字段
    const { id } = await tableInstance.getField(fieldName);
    return id;
  } catch (error) {
    // 字段不存在,创建新字段
    const fieldId = await tableInstance.addField({ type: fieldType, name: fieldName });
    return fieldId;
  }
}

使用示例:

const fieldId = await getFieldId(table, '镜头', FieldType.Text);

3️⃣ 批量添加记录

功能说明:按字段映射关系将对象数组批量写入表格,相比逐条添加效率更高。

/**
 * 批量添加记录到表格
 * @description 按字段映射关系将对象数组批量写入表格,相比逐条添加效率更高
 * @param {Object} tableInstance - 表格实例对象
 * @param {Array<Object>} dataList - 要添加的数据列表,每个元素为一个数据对象
 * @param {Array<Object>} tableFields - 字段映射配置数组
 * @param {string} tableFields[].field_name - 表格中的目标字段名
 * @param {string} tableFields[].field_value - 数据对象中的源字段名
 * @returns {Promise<Array<string>>} 返回所有添加记录的ID数组
 * @example
 * const dataList = [
 *   { name: '张三', age: 25, city: '北京' },
 *   { name: '李四', age: 30, city: '上海' }
 * ];
 * const tableFields = [
 *   { field_name: '姓名', field_value: 'name' },
 *   { field_name: '年龄', field_value: 'age' },
 *   { field_name: '城市', field_value: 'city' }
 * ];
 * const recordIds = await addRecordsToTable(table, dataList, tableFields);
 */
export async function addRecordsToTable(tableInstance, dataList, tableFields) {
  const allRecordIds = [] // 存储所有添加的记录ID
  // 遍历每一行数据
  for (const rowData of dataList) {
    const textCellList = [] // 存储当前行的所有单元格
    // 根据字段映射创建单元格
    for (const item of tableFields) {
      const fieldName = item.field_name // 目标字段名
      const fieldValue = item.field_value // 数据源字段名
      const textField = await tableInstance.getField(fieldName); // 获取字段实例
      const cellValue = rowData[fieldValue] || ''; // 获取单元格值
      const textCell = await textField.createCell(cellValue); // 创建单元格
      textCellList.push(textCell) // 添加到单元格列表
    }
    // 将当前行的所有单元格添加为一条记录
    const recordIds = await tableInstance.addRecords(textCellList);
    allRecordIds.push(...recordIds) // 收集记录ID
  }
  return allRecordIds // 返回所有记录ID
}

映射配置示例:

const tableFields = [
  { field_name: '镜头', field_value: 'shot' },
  { field_name: '场景类型', field_value: 'sceneType' },
  { field_name: '时长', field_value: 'duration' },
  { field_name: '内容', field_value: 'content' },
  { field_name: '对话', field_value: 'dialogue' }
];

const dataList = [
  { shot: '第1镜', sceneType: '室内', duration: '30秒', content: '主角进入房间', dialogue: '你好,我回来了' },
  { shot: '第2镜', sceneType: '室外', duration: '45秒', content: '街道场景', dialogue: '再见' }
];

await addRecordsToTable(table, dataList, tableFields);

4️⃣ 确保字段存在

功能说明:自动检查并创建缺失的字段,避免因字段不存在导致的数据写入失败。

/**
 * 确保字段存在,不存在则自动创建
 * @description 检查表格中是否存在指定字段,如不存在则按指定类型创建。对于单选字段,会自动添加选项
 * @param {Object} tableInstance - 表格实例对象
 * @param {Array<Object>} fieldConfigs - 字段配置数组
 * @param {string} fieldConfigs[].name - 字段名称
 * @param {FieldType} fieldConfigs[].type - 字段类型
 * @param {Object} fieldConfigs[].property - 字段属性配置(可选)
 *   - 对于单选字段:property.options 为 { name: string, color?: number }[] 格式
 *   - 对于其他字段:根据字段类型传入相应的配置对象
 * @returns {Promise<Array<Object>>} 返回所有字段实例数组
 * @example
 * const fieldConfigs = [
 *   { name: '姓名', type: FieldType.Text },
 *   { name: '年龄', type: FieldType.Number },
 *   { 
 *     name: '状态', 
 *     type: FieldType.SingleSelect, 
 *     property: { 
 *       options: [
 *         { name: '进行中', color: 0 },
 *         { name: '已完成', color: 1 }
 *       ]
 *     }
 *   }
 * ];
 * const fields = await ensureFieldsExist(table, fieldConfigs);
 */
export async function ensureFieldsExist(tableInstance, fieldConfigs) {
  const fieldInstances = []
  
  for (const config of fieldConfigs) {
    try {
      // 尝试获取已存在的字段
      const existingField = await tableInstance.getField(config.name)
      fieldInstances.push(existingField)
    } catch (error) {
      // 字段不存在,创建新字段
      const newField = await tableInstance.addField({
        type: config.type,
        name: config.name,
        property: config.property || {}
      })
      // 如果是单选字段且有选项配置,添加选项
      if (config.type === FieldType.SingleSelect && config.property?.options && Array.isArray(config.property.options)) {
        await newField.addOptions(config.property.options)
      }
      fieldInstances.push(newField)
    }
  }
  
  return fieldInstances
}

使用示例:

import { FieldType } from '@lark-base-open/js-sdk';

const fieldConfigs = [
  { name: '项目名称', type: FieldType.Text },
  { 
    name: '优先级', 
    type: FieldType.SingleSelect, 
    property: { 
      options: [
        { name: '高', color: 0 },
        { name: '中', color: 1 },
        { name: '低', color: 2 }
      ]
    }
  },
  { name: '完成度', type: FieldType.Number },
  { name: '截止日期', type: FieldType.DateTime }
];

const fields = await ensureFieldsExist(table, fieldConfigs);
console.log('所有字段准备就绪!');

5️⃣ 分批处理大量数据

功能说明:将大量数据分批处理,避免一次性操作过多数据导致的接口超时或性能问题。

/**
 * 分批添加记录到表格
 * @description 将大量数据分批处理,避免一次性操作过多数据导致超时
 * @param {Object} tableInstance - 表格实例对象
 * @param {Array<Object>} dataList - 要添加的数据列表
 * @param {Array<Object>} tableFields - 字段映射配置数组
 * @param {Object} options - 配置选项
 * @param {number} options.batchSize - 每批处理的记录数,默认50
 * @param {number} options.delay - 批次间延迟时间(毫秒),默认100
 * @param {Function} options.onProgress - 进度回调函数
 * @returns {Promise<Array<string>>} 返回所有添加记录的ID数组
 * @example
 * const recordIds = await chunkedAddRecords(table, dataList, tableFields, {
 *   batchSize: 30,
 *   delay: 200,
 *   onProgress: (current, total) => console.log(`进度: ${current}/${total}`)
 * });
 */
export async function chunkedAddRecords(tableInstance, dataList, tableFields, options = {}) {
  const { batchSize = 50, delay = 100, onProgress } = options
  const allRecordIds = []
  const totalBatches = Math.ceil(dataList.length / batchSize)
  
  for (let i = 0; i < dataList.length; i += batchSize) {
    const batch = dataList.slice(i, i + batchSize)
    const currentBatch = Math.floor(i / batchSize) + 1
    
    // 处理当前批次
    const batchRecordIds = await addRecordsToTable(tableInstance, batch, tableFields)
    allRecordIds.push(...batchRecordIds)
    
    // 进度回调
    if (onProgress) {
      onProgress(i + batch.length, dataList.length)
    }
    
    // 批次间延迟,避免接口压力过大
    if (i + batchSize < dataList.length && delay > 0) {
      await new Promise(resolve => setTimeout(resolve, delay))
    }
  }
  
  return allRecordIds
}

使用示例:

// 处理大量数据(比如1000条记录)
const largeDataList = [...]; // 1000条数据
const tableFields = [
  { field_name: '姓名', field_value: 'name' },
  { field_name: '部门', field_value: 'department' }
];

const recordIds = await chunkedAddRecords(table, largeDataList, tableFields, {
  batchSize: 30,        // 每批30条
  delay: 200,           // 批次间延迟200ms
  onProgress: (current, total) => {
    const percent = Math.round((current / total) * 100);
    console.log(`导入进度: ${percent}% (${current}/${total})`);
  }
});

6️⃣ 文本解析转对象数组

功能说明:将制表符分隔的文本(如从Excel复制的数据)解析为对象数组,支持自动识别表头和数据行,是数据导入的核心工具。

/**
 * 将制表符分隔的文本解析为对象数组
 * @description 解析从Excel或表格复制的制表符分隔文本,第一行作为表头
 * @param {string} text - 制表符分隔的文本内容
 * @param {Object} options - 解析选项
 * @param {string} options.delimiter - 分隔符,默认为制表符
 * @param {boolean} options.hasHeader - 是否包含表头,默认true
 * @param {boolean} options.trimValues - 是否去除值的首尾空格,默认true
 * @returns {Array<Object>} 解析后的对象数组
 * @example
 * const text = `姓名\t年龄\t城市\n张三\t25\t北京\n李四\t30\t上海`;
 * const objects = parseTabularTextToObjects(text);
 * // 返回: [
 * //   { 姓名: '张三', 年龄: '25', 城市: '北京' },
 * //   { 姓名: '李四', 年龄: '30', 城市: '上海' }
 * // ]
 */
export function parseTabularTextToObjects(text, options = {}) {
  const { delimiter = '\t', hasHeader = true, trimValues = true } = options
  
  if (!text || typeof text !== 'string') {
    return []
  }
  
  // 按行分割文本
  const lines = text.split('\n').filter(line => line.trim())
  
  if (lines.length === 0) {
    return []
  }
  
  // 获取表头
  const headers = lines[0].split(delimiter).map(header => 
    trimValues ? header.trim() : header
  )
  
  if (!hasHeader) {
    // 如果没有表头,使用列索引作为键名
    headers = headers.map((_, index) => `column_${index}`)
  }
  
  // 解析数据行
  const dataLines = hasHeader ? lines.slice(1) : lines
  const objects = []
  
  for (let i = 0; i < dataLines.length; i++) {
    const values = dataLines[i].split(delimiter)
    const obj = {}
    
    headers.forEach((header, index) => {
      const value = values[index] || ''
      obj[header] = trimValues ? value.trim() : value
    })
    
    objects.push(obj)
  }
  
  return objects
}

使用示例:

// 从剪贴板获取的文本
const clipboardText = `项目名称负责人状态优先级
网站重构张三进行中高
移动端开发李四已完成中
数据分析王五待开始低`;

// 解析文本
const projects = parseTabularTextToObjects(clipboardText);
console.log('解析结果:', projects);
// 输出:[
//   { 项目名称: '网站重构', 负责人: '张三', 状态: '进行中', 优先级: '高' },
//   { 项目名称: '移动端开发', 负责人: '李四', 状态: '已完成', 优先级: '中' },
//   { 项目名称: '数据分析', 负责人: '王五', 状态: '待开始', 优先级: '低' }
// ]

// 自定义分隔符解析CSV
const csvText = `姓名,年龄,部门\n张三,25,技术部\n李四,30,产品部`;
const employees = parseTabularTextToObjects(csvText, { delimiter: ',' });

7️⃣ 条件查询记录

功能说明:根据指定条件查询表格记录,支持多字段组合查询、模糊匹配等,是数据筛选和分析的基础工具。

/**
 * 根据条件查询表格记录
 * @description 支持多字段条件查询,可进行精确匹配或模糊匹配
 * @param {Object} tableInstance - 表格实例对象
 * @param {Array<Object>} conditions - 查询条件数组
 * @param {string} conditions[].fieldName - 字段名称
 * @param {any} conditions[].value - 查询值
 * @param {string} conditions[].operator - 操作符:'equals'(精确)、'contains'(包含)、'startsWith'(开头)、'endsWith'(结尾)
 * @param {string} logic - 条件间逻辑关系:'AND' 或 'OR',默认'AND'
 * @returns {Promise<Array<Object>>} 返回符合条件的记录数组
 * @example
 * const conditions = [
 *   { fieldName: '状态', value: '进行中', operator: 'equals' },
 *   { fieldName: '负责人', value: '张', operator: 'contains' }
 * ];
 * const records = await queryRecordsByConditions(table, conditions, 'AND');
 */
export async function queryRecordsByConditions(tableInstance, conditions, logic = 'AND') {
  try {
    // 获取所有记录
    const recordList = await tableInstance.getRecords({
      pageSize: 5000  // 获取足够多的记录
    })
    
    const matchedRecords = []
    
    for (const record of recordList.records) {
      let isMatch = logic === 'AND' ? true : false
      
      for (const condition of conditions) {
        const { fieldName, value, operator = 'equals' } = condition
        
        // 获取字段值
        const field = await tableInstance.getField(fieldName)
        const cellValue = await record.getCellValueString(field.id)
        
        // 执行匹配检查
        let conditionMatch = false
        switch (operator) {
          case 'equals':
            conditionMatch = cellValue === String(value)
            break
          case 'contains':
            conditionMatch = cellValue.includes(String(value))
            break
          case 'startsWith':
            conditionMatch = cellValue.startsWith(String(value))
            break
          case 'endsWith':
            conditionMatch = cellValue.endsWith(String(value))
            break
          default:
            conditionMatch = cellValue === String(value)
        }
        
        // 根据逻辑关系更新匹配状态
        if (logic === 'AND') {
          isMatch = isMatch && conditionMatch
          if (!isMatch) break  // AND逻辑下,一个不匹配就可以跳出
        } else {
          isMatch = isMatch || conditionMatch
          if (isMatch) break   // OR逻辑下,一个匹配就可以跳出
        }
      }
      
      if (isMatch) {
        // 构建记录对象,包含所有字段值
        const recordData = { recordId: record.recordId }
        const fieldMetaList = await tableInstance.getFieldMetaList()
        
        for (const fieldMeta of fieldMetaList) {
          const cellValue = await record.getCellValueString(fieldMeta.id)
          recordData[fieldMeta.name] = cellValue
        }
        
        matchedRecords.push(recordData)
      }
    }
    
    return matchedRecords
    
  } catch (error) {
    throw error
  }
}

使用示例:

// 查询状态为"进行中"且负责人包含"张"的记录
const conditions = [
  { fieldName: '状态', value: '进行中', operator: 'equals' },
  { fieldName: '负责人', value: '张', operator: 'contains' }
];

const records = await queryRecordsByConditions(table, conditions, 'AND');
console.log('查询结果:', records);

// 查询优先级为"高"或"紧急"的记录
const urgentConditions = [
  { fieldName: '优先级', value: '高', operator: 'equals' },
  { fieldName: '优先级', value: '紧急', operator: 'equals' }
];

const urgentRecords = await queryRecordsByConditions(table, urgentConditions, 'OR');

8️⃣ 批量删除记录

功能说明:根据条件批量删除表格记录,支持条件筛选删除和记录ID列表删除,操作前会进行安全确认。

/**
 * 批量删除表格记录
 * @description 根据条件或记录ID列表批量删除记录,支持安全确认
 * @param {Object} tableInstance - 表格实例对象
 * @param {Object} options - 删除选项
 * @param {Array<string>} options.recordIds - 要删除的记录ID数组(优先使用)
 * @param {Array<Object>} options.conditions - 删除条件数组(当recordIds为空时使用)
 * @param {boolean} options.confirm - 是否需要确认,默认true
 * @param {boolean} options.dryRun - 是否为试运行(只查询不删除),默认false
 * @returns {Promise<Object>} 返回删除结果统计
 * @example
 * // 按记录ID删除
 * const result = await batchDeleteRecords(table, {
 *   recordIds: ['rec123', 'rec456'],
 *   confirm: false
 * });
 * 
 * // 按条件删除
 * const result = await batchDeleteRecords(table, {
 *   conditions: [{ fieldName: '状态', value: '已废弃', operator: 'equals' }],
 *   dryRun: true  // 先试运行看看会删除哪些记录
 * });
 */
export async function batchDeleteRecords(tableInstance, options = {}) {
  const { recordIds, conditions, confirm = true, dryRun = false } = options
  
  try {
    let targetRecords = []
    
    // 确定要删除的记录
    if (recordIds && recordIds.length > 0) {
      // 按记录ID删除
      targetRecords = recordIds.map(id => ({ recordId: id }))
    } else if (conditions && conditions.length > 0) {
      // 按条件查询要删除的记录
      const queryResults = await queryRecordsByConditions(tableInstance, conditions)
      targetRecords = queryResults
    } else {
      throw new Error('必须提供 recordIds 或 conditions 参数')
    }
    
    if (targetRecords.length === 0) {
      return { deleted: 0, skipped: 0, errors: 0 }
    }
    
    // 试运行模式
    if (dryRun) {
      targetRecords.forEach((record, index) => {
        console.log(`${index + 1}. 记录ID: ${record.recordId}`)
      })
      return { 
        deleted: 0, 
        skipped: targetRecords.length, 
        errors: 0,
        preview: targetRecords 
      }
    }
    
    // 安全确认
    if (confirm) {
      const confirmMessage = `⚠️ 即将删除 ${targetRecords.length} 条记录,此操作不可撤销!确认继续吗?`
      console.warn(confirmMessage)
      // TODO:在实际应用中,这里应该弹出确认对话框
    }
    
    // 执行删除
    const deleteResults = { deleted: 0, skipped: 0, errors: 0 }
    
    // 分批删除,避免一次删除过多
    const batchSize = 50
    for (let i = 0; i < targetRecords.length; i += batchSize) {
      const batch = targetRecords.slice(i, i + batchSize)
      const batchIds = batch.map(record => record.recordId)
      
      try {
        await tableInstance.deleteRecords(batchIds)
        deleteResults.deleted += batchIds.length
      } catch (error) {
        deleteResults.errors += batchIds.length
      }
    }
    
    return deleteResults
  } catch (error) {
    throw error
  }
}

使用示例:

// 删除所有状态为"已废弃"的记录(先试运行)
const previewResult = await batchDeleteRecords(table, {
  conditions: [{ fieldName: '状态', value: '已废弃', operator: 'equals' }],
  dryRun: true
});
console.log('预览删除结果:', previewResult);

// 确认后执行删除
if (previewResult.preview.length > 0) {
  const deleteResult = await batchDeleteRecords(table, {
    conditions: [{ fieldName: '状态', value: '已废弃', operator: 'equals' }],
    confirm: false  // 已经预览过了,跳过确认
  });
}

// 按记录ID删除
const specificIds = ['rec123', 'rec456', 'rec789'];
await batchDeleteRecords(table, {
  recordIds: specificIds,
  confirm: false
});

9️⃣ 数据导出功能

功能说明:将表格数据导出为多种格式(JSON、CSV、TSV、Excel),支持字段筛选、条件过滤和格式化选项。

/**
 * 导出表格数据
 * @description 将表格数据导出为指定格式,支持字段筛选和条件过滤
 * @param {Object} tableInstance - 表格实例对象
 * @param {Object} options - 导出选项
 * @param {string} options.format - 导出格式:'json'、'csv'、'tsv'(制表符分隔)、'excel'(.xlsx格式),默认'json'
 * @param {Array<string>} options.fields - 要导出的字段名数组,为空则导出所有字段
 * @param {Array<Object>} options.conditions - 过滤条件数组,为空则导出所有记录
 * @param {boolean} options.includeHeader - CSV/TSV/Excel格式是否包含表头,默认true
 * @param {string} options.filename - 导出文件名(不含扩展名)
 * @param {string} options.sheetName - Excel工作表名称,默认'Sheet1'
 * @returns {Promise<Object>} 返回导出结果,包含数据和下载链接
 * @example
 * // 导出为JSON格式
 * const result = await exportTableData(table, {
 *   format: 'json',
 *   fields: ['姓名', '部门', '状态'],
 *   filename: '员工数据'
 * });
 * 
 * // 导出符合条件的记录为CSV
 * const result = await exportTableData(table, {
 *   format: 'csv',
 *   conditions: [{ fieldName: '状态', value: '在职', operator: 'equals' }],
 *   filename: '在职员工'
 * });
 * 
 * // 导出为Excel格式
 * const excelResult = await exportTableData(table, {
 *   format: 'excel',
 *   fields: ['姓名', '部门', '状态'],
 *   filename: '员工数据',
 *   sheetName: '员工信息'
 * });
 */
export async function exportTableData(tableInstance, options = {}) {
  const { 
    format = 'json', 
    fields = [], 
    conditions = [], 
    includeHeader = true,
    filename = 'export_data',
    sheetName = 'Sheet1'
  } = options
  
  try {
    // 获取要导出的记录
    let records = []
    if (conditions.length > 0) {
      records = await queryRecordsByConditions(tableInstance, conditions)
    } else {
      const recordList = await tableInstance.getRecords({ pageSize: 5000 })
      
      // 获取所有字段信息
      const fieldMetaList = await tableInstance.getFieldMetaList()
      
      for (const record of recordList.records) {
        const recordData = { recordId: record.recordId }
        
        for (const fieldMeta of fieldMetaList) {
          const cellValue = await record.getCellValueString(fieldMeta.id)
          recordData[fieldMeta.name] = cellValue
        }
        
        records.push(recordData)
      }
    }
    
    if (records.length === 0) {
      return { success: false, message: '没有数据可导出' }
    }
    
    // 确定要导出的字段
    let exportFields = fields
    if (exportFields.length === 0) {
      // 如果没有指定字段,导出所有字段(除了recordId)
      exportFields = Object.keys(records[0]).filter(key => key !== 'recordId')
    }
    
    // 过滤记录,只保留指定字段
    const filteredRecords = records.map(record => {
      const filteredRecord = {}
      exportFields.forEach(field => {
        filteredRecord[field] = record[field] || ''
      })
      return filteredRecord
    })
    
    // 根据格式生成导出内容
    let exportContent = ''
    let mimeType = 'text/plain'
    let fileExtension = 'txt'
    
    switch (format.toLowerCase()) {
      case 'json':
        exportContent = JSON.stringify(filteredRecords, null, 2)
        mimeType = 'application/json'
        fileExtension = 'json'
        break
        
      case 'csv':
        exportContent = convertToCSV(filteredRecords, exportFields, includeHeader)
        mimeType = 'text/csv'
        fileExtension = 'csv'
        break
        
      case 'tsv':
        exportContent = convertToTSV(filteredRecords, exportFields, includeHeader)
        mimeType = 'text/tab-separated-values'
        fileExtension = 'tsv'
        break
        
      case 'excel':
        exportContent = convertToExcel(filteredRecords, exportFields, includeHeader, sheetName)
        mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
        fileExtension = 'xlsx'
        break
        
      default:
        throw new Error(`不支持的导出格式: ${format}`)
    }
    
    // 创建下载链接
    const blob = new Blob([exportContent], { type: mimeType })
    const downloadUrl = URL.createObjectURL(blob)
    const fullFilename = `${filename}.${fileExtension}`
    
    return {
      success: true,
      data: filteredRecords,
      content: exportContent,
      downloadUrl: downloadUrl,
      filename: fullFilename,
      recordCount: filteredRecords.length,
      fieldCount: exportFields.length
    }
    
  } catch (error) {
    throw error
  }
}

// 辅助函数:转换为CSV格式
function convertToCSV(records, fields, includeHeader) {
  const lines = []
  
  // 添加表头
  if (includeHeader) {
    lines.push(fields.map(field => `"${field}"`).join(','))
  }
  
  // 添加数据行
  records.forEach(record => {
    const values = fields.map(field => {
      const value = record[field] || ''
      // TODO: CSV格式需要转义双引号
      // return `"${String(value).replace(/"/g, '""')}"`
    })
    lines.push(values.join(','))
  })
  
  return lines.join('\n')
}

// 辅助函数:转换为TSV格式
function convertToTSV(records, fields, includeHeader) {
  const lines = []
  
  // 添加表头
  if (includeHeader) {
    lines.push(fields.join('\t'))
  }
  
  // 添加数据行
  records.forEach(record => {
    const values = fields.map(field => {
      const value = record[field] || ''
      // TSV格式需要转义制表符和换行符
      return String(value).replace(/\t/g, ' ').replace(/\n/g, ' ')
    })
    lines.push(values.join('\t'))
  })
  
  return lines.join('\n')
}

// 辅助函数:转换为Excel格式(.xlsx);前端导出Excel可以使用一些现成的库,这里小编由于某些原因只能使用JS来完成,所以让AI用纯JS撸了一个来满足需求。。。
function convertToExcel(records, fields, includeHeader, sheetName = 'Sheet1') {
  // Excel文件的基本XML结构
  const xmlHeader = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
  
  // 构建工作表数据
  let sheetData = ''
  let rowIndex = 1
  
  // 添加表头
  if (includeHeader) {
    sheetData += `<row r="${rowIndex}">`
    fields.forEach((field, colIndex) => {
      const cellRef = getCellReference(rowIndex, colIndex + 1)
      sheetData += `<c r="${cellRef}" t="inlineStr"><is><t>${escapeXml(field)}</t></is></c>`
    })
    sheetData += '</row>'
    rowIndex++
  }
  
  // 添加数据行
  records.forEach(record => {
    sheetData += `<row r="${rowIndex}">`
    fields.forEach((field, colIndex) => {
      const cellRef = getCellReference(rowIndex, colIndex + 1)
      const value = record[field] || ''
      const cellValue = String(value)
      
      // 判断是否为数字
      const isNumber = !isNaN(cellValue) && !isNaN(parseFloat(cellValue)) && cellValue.trim() !== ''
      
      if (isNumber) {
        sheetData += `<c r="${cellRef}"><v>${cellValue}</v></c>`
      } else {
        sheetData += `<c r="${cellRef}" t="inlineStr"><is><t>${escapeXml(cellValue)}</t></is></c>`
      }
    })
    sheetData += '</row>'
    rowIndex++
  })
  
  // 完整的工作表XML
  const worksheet = `${xmlHeader}
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
  <sheetData>
    ${sheetData}
  </sheetData>
</worksheet>`
  
  // 创建ZIP文件结构(简化版Excel文件)
  const zipContent = createExcelZip(worksheet, sheetName)
  
  return zipContent
}

// 辅助函数:获取Excel单元格引用(如A1, B2等)
function getCellReference(row, col) {
  let colName = ''
  while (col > 0) {
    col--
    colName = String.fromCharCode(65 + (col % 26)) + colName
    col = Math.floor(col / 26)
  }
  return colName + row
}

// 辅助函数:转义XML特殊字符
function escapeXml(text) {
  return String(text)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;')
}

// 辅助函数:创建Excel ZIP文件结构
function createExcelZip(worksheet, sheetName) {
  // 这里使用简化的方法,实际上Excel文件是一个ZIP包含多个XML文件
  // 为了不使用外部库,我们创建一个包含基本结构的XML文件
  // 注意:这是一个简化版本,可能不被所有Excel版本完全支持
  
  const contentTypes = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
  <Default Extension="xml" ContentType="application/xml"/>
  <Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
  <Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
</Types>`

  const workbook = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
  <sheets>
    <sheet name="${escapeXml(sheetName)}" sheetId="1" r:id="rId1" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"/>
  </sheets>
</workbook>`

  // 由于不使用外部库,我们返回一个包含所有必要信息的数据结构
  // 实际使用时,这需要被正确地打包成ZIP格式
  // 这里我们返回一个特殊格式的字符串,包含所有必要的Excel文件内容
  return JSON.stringify({
    '[Content_Types].xml': contentTypes,
    'xl/workbook.xml': workbook,
    'xl/worksheets/sheet1.xml': worksheet,
    '_format': 'excel-json' // 标识这是Excel JSON格式
  })
}

使用示例:

// 导出所有数据为JSON格式
const jsonResult = await exportTableData(table, {
  format: 'json',
  filename: '完整数据'
});

// 导出指定字段为CSV格式
const csvResult = await exportTableData(table, {
  format: 'csv',
  fields: ['姓名', '部门', '入职日期'],
  filename: '员工基本信息'
});

// 导出符合条件的记录
const filteredResult = await exportTableData(table, {
  format: 'tsv',
  conditions: [
    { fieldName: '状态', value: '在职', operator: 'equals' },
    { fieldName: '部门', value: '技术', operator: 'contains' }
  ],
  filename: '技术部在职员工'
});

// 导出为Excel格式
const excelResult = await exportTableData(table, {
  format: 'excel',
  fields: ['姓名', '部门', '入职日期', '状态'],
  filename: '员工信息表',
  sheetName: '员工数据'
});

// 触发下载
if (csvResult.success) {
  const link = document.createElement('a');
  link.href = csvResult.downloadUrl;
  link.download = csvResult.filename;
  link.click();
}

// Excel文件下载(需要特殊处理)
if (excelResult.success) {
  // 注意:由于Excel格式的复杂性,实际使用时可能需要额外的处理
  // 这里提供的是基础的XML结构,可以被大多数Excel应用程序识别
  const link = document.createElement('a');
  link.href = excelResult.downloadUrl;
  link.download = excelResult.filename;
  link.click();
}

🔟 智能字段映射写入

功能说明:结合字段检查、数据解析和批量写入的综合解决方案,实现从文本到表格的一站式智能导入,自动处理字段创建和数据类型转换。

/**
 * 智能字段映射写入数据
 * @description 综合解决方案:解析文本 → 检查字段 → 创建缺失字段 → 批量写入数据
 * @param {Object} tableInstance - 表格实例对象
 * @param {string} textData - 要导入的文本数据(制表符分隔)
 * @param {Array<Object>} fieldMappings - 字段映射配置
 * @param {string} fieldMappings[].name - 目标字段名
 * @param {string} fieldMappings[].valueKey - 数据源字段名
 * @param {FieldType} fieldMappings[].type - 字段类型
 * @param {Object} fieldMappings[].property - 字段属性配置(可选)
 * @param {Object} options - 导入选项
 * @param {number} options.batchSize - 批次大小,默认50
 * @param {boolean} options.autoCreateFields - 是否自动创建缺失字段,默认true
 * @param {Function} options.onProgress - 进度回调函数
 * @returns {Promise<Object>} 返回导入结果统计
 * @example
 * const textData = `项目名称\t负责人\t状态\t优先级
 * 网站重构\t张三\t进行中\t高
 * 移动端开发\t李四\t已完成\t中`;
 * 
 * const fieldMappings = [
 *   { name: '项目名称', valueKey: '项目名称', type: FieldType.Text },
 *   { name: '负责人', valueKey: '负责人', type: FieldType.Text },
 *   { 
 *     name: '状态', 
 *     valueKey: '状态', 
 *     type: FieldType.SingleSelect, 
 *     property: { 
 *       options: [
 *         { name: '进行中', color: 0 },
 *         { name: '已完成', color: 1 },
 *         { name: '待开始', color: 2 }
 *       ]
 *     }
 *   },
 *   { 
 *     name: '优先级', 
 *     valueKey: '优先级', 
 *     type: FieldType.SingleSelect, 
 *     property: { 
 *       options: [
 *         { name: '高', color: 0 },
 *         { name: '中', color: 1 },
 *         { name: '低', color: 2 }
 *       ]
 *     }
 *   }
 * ];
 * 
 * const result = await smartFieldMappingImport(table, textData, fieldMappings, {
 *   batchSize: 30,
 *   onProgress: (current, total) => console.log(`导入进度: ${current}/${total}`)
 * });
 */
export async function smartFieldMappingImport(tableInstance, textData, fieldMappings, options = {}) {
  const { 
    batchSize = 50, 
    autoCreateFields = true, 
    onProgress 
  } = options
  
  try {
    // 第1步:解析文本数据
    const parsedData = parseTabularTextToObjects(textData)
    if (parsedData.length === 0) {
      throw new Error('没有解析到有效数据')
    }
    // 第2步:检查并创建字段
    if (autoCreateFields) {
      const fieldConfigs = fieldMappings.map(mapping => ({
        name: mapping.name,
        type: mapping.type,
        property: mapping.property
      }))
      await ensureFieldsExist(tableInstance, fieldConfigs)
    }
    // 第3步:准备字段映射配置
    const tableFields = fieldMappings.map(mapping => ({
      field_name: mapping.name,
      field_value: mapping.valueKey
    }))
    // 第4步:批量写入数据
    const recordIds = await chunkedAddRecords(tableInstance, parsedData, tableFields, {
      batchSize,
      onProgress
    })
    // 第5步:生成导入报告
    const importResult = {
      success: true,
      totalRecords: parsedData.length,
      importedRecords: recordIds.length,
      failedRecords: parsedData.length - recordIds.length,
      fieldCount: fieldMappings.length,
      recordIds: recordIds,
      summary: {
        parseTime: new Date().toISOString(),
        fieldsCreated: autoCreateFields ? fieldMappings.length : 0,
        batchSize: batchSize
      }
    }
    return importResult
  } catch (error) {
    return {
      success: false,
      error: error.message,
      totalRecords: 0,
      importedRecords: 0,
      failedRecords: 0
    }
  }
}

使用示例:

import { FieldType } from '@lark-base-open/js-sdk';

// 从剪贴板或文件获取的数据
const projectData = `项目名称负责人状态优先级开始日期
网站重构张三进行中高2024-01-15
移动端开发李四已完成中2024-01-10
数据分析王五待开始低2024-01-20
API开发赵六进行中高2024-01-12`;

// 定义字段映射和类型
const fieldMappings = [
  { 
    name: '项目名称', 
    valueKey: '项目名称', 
    type: FieldType.Text 
  },
  { 
    name: '负责人', 
    valueKey: '负责人', 
    type: FieldType.Text 
  },
  { 
    name: '状态', 
    valueKey: '状态', 
    type: FieldType.SingleSelect, 
    property: { 
      options: [
        { name: '进行中', color: 0 },
        { name: '已完成', color: 1 },
        { name: '待开始', color: 2 },
        { name: '已暂停', color: 3 }
      ]
    }
  },
  { 
    name: '优先级', 
    valueKey: '优先级', 
    type: FieldType.SingleSelect, 
    property: { 
      options: [
        { name: '高', color: 0 },
        { name: '中', color: 1 },
        { name: '低', color: 2 },
        { name: '紧急', color: 3 }
      ]
    }
  },
  { 
    name: '开始日期', 
    valueKey: '开始日期', 
    type: FieldType.DateTime 
  }
];

// 执行智能导入
const importResult = await smartFieldMappingImport(table, projectData, fieldMappings, {
  batchSize: 20,
  autoCreateFields: true,
  onProgress: (current, total) => {
    const percent = Math.round((current / total) * 100);
  }
});

// 检查导入结果
if (importResult.success) {
  console.log('导入详情:', importResult);
} else {
  console.error('导入失败:', importResult.error);
}

// 一键导入Excel数据示例
async function quickImportFromClipboard() {
  try {
    // 从剪贴板读取数据
    const clipboardText = await navigator.clipboard.readText();
    
    // 自动识别字段并创建映射
    const lines = clipboardText.split('\n');
    const headers = lines[0].split('\t');
    
    const autoFieldMappings = headers.map(header => ({
      name: header.trim(),
      valueKey: header.trim(),
      type: FieldType.Text  // 默认为文本类型,可根据需要调整
    }));
    
    // 执行导入
    const result = await smartFieldMappingImport(table, clipboardText, autoFieldMappings);
  } catch (error) {
    console.error('快速导入失败:', error);
  }
}




至此,本篇文章就写完啦,撒花撒花。

image.png

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。

Xrecode3(多功能音频转换工具)

作者 非凡ghost
2025年10月29日 10:12

Xrecode3是一款功能强大的音频转换工具,它能够将多种音频格式相互转换,并提供了许多其他的音频处理功能。

软件功能

音频格式转换:支持将大多数常见的音频格式互相转换,包括MP3、FLAC、WAV、AAC、OGG等。
批量转换:可以同时转换多个文件,节省大量时间和精力。
支持多通道的音频:可以处理多通道音频文件,并保存每个通道的独立文件。
提供了高质量的音频编码器:内置了多种高质量音频编解码器,保证转换过程中音质不损失。
支持CUE文件:能够直接读取CUE文件,并根据CUE文件分割音轨。
ID3标签编辑:可以编辑音频文件的ID3标签,包括歌曲标题、艺术家、专辑等信息。
内置音频播放器:可以通过内置音频播放器来预览转换后的音频文件。
提供了音频剪辑功能:可以从音频文件中剪切出所需部分。
支持多核处理器:利用多核处理器的优势,加快转换速度。

软件特点

界面简洁直观:操作简单易懂,即使对音频处理不熟悉的人也能够轻松上手。
多种输出配置:提供了许多输出配置选项,用户可以根据需要自定义输出文件的音频格式、采样率、码率等参数。
高度可定制化:支持设置转换任务的优先级,可以根据需要调整转换的顺序。
快速转换速度:利用了硬件加速和多线程处理技术,大大提高了转换速度。
资源占用少:在转换过程中占用的系统资源较少,运行稳定、流畅。

「Xrecode3(多功能音频转换工具) 」 链接:pan.quark.cn/s/1480b6f1d…

现代化企业级H5模板:Vite + Vue 3 + TypeScript 开箱即用

作者 wuxingxi
2025年10月29日 10:06

现代化企业级H5模板:Vite + Vue 3 + TypeScript 开箱即用

作为一名前端开发者,你是否厌倦了每次开始新项目时都要重新配置繁琐的工程化环境?是否希望有一个开箱即用、集成现代前端最佳实践的H5模板?今天给大家推荐一个非常棒的开源项目——vite-vue3-h5-template,它能帮你快速搭建高质量的移动端H5项目。

项目简介

vite-vue3-h5-template 是一个基于 Vue 3、Vite、TypeScript 和 Pinia 构建的现代化企业级移动端 H5 模板。该项目采用了前沿的技术栈和工程化方案,专为追求高性能、可扩展性和可维护性的移动 Web 应用开发而设计。

无论你是独立开发者还是团队协作,这个模板都能为你提供强大的支撑和便利的开发体验。

核心特性一览

🚀 现代化技术栈

项目集成了当前最热门的前端技术:

  • Vue 3 Composition API - 更灵活的组件组织方式
  • Vite 7 - 极速的构建工具,开发体验飞一般
  • TypeScript - 强类型支持,提高代码质量和开发效率
  • Pinia - Vue官方推荐的状态管理方案
  • UnoCSS - 高性能原子化CSS引擎

🏗️ Monorepo架构

采用 Monorepo 架构,通过 Turbo 和 pnpm 工作区进行管理,不仅包含了主H5应用,还集成了文档站点和可复用的配置包,便于统一管理和维护。

💻 卓越开发体验

  • Plop 代码生成器 - 一键生成组件、页面和store模块
  • ESLint/Prettier - 统一代码风格,保证代码质量
  • Husky & lint-staged - Git提交前自动校验
  • SVG 图标自动注册 - 使用SVG图标更加方便
  • Mock 开发 - 无需等待后端接口即可开发调试

📱 移动端专项优化

  • Viewport 自适应方案 - 完美适配各种移动设备
  • 触摸模拟器 - 在PC上也能模拟触控行为
  • @miracle-web/ui 组件库 - 专为移动端设计的Vue3组件库
  • 移动端交互优化 - 输入框防遮挡、状态栏适配等

⚡ 性能优化

  • 代码分割和懒加载 - 提升首屏加载速度
  • 构建优化 - 多种优化策略减小打包体积
  • 自动组件注册 - 只打包用到的组件

快速上手

只需几步即可开始你的项目开发:

# 克隆项目
git clone https://github.com/wuxingxi888/vite-vue3-h5-template.git

# 进入项目目录
cd vite-vue3-h5-template

# 安装依赖(需要先安装pnpm)
curl -fsSL https://get.pnpm.io/install.sh | sh -
pnpm install

# 启动开发服务器
pnpm dev

现在你就可以在浏览器中访问你的H5应用了!

项目结构清晰

.
├── apps/
│   ├── h5-template/     # 主 H5 应用源码
│   └── docs/            # 文档站点 (VitePress)
├── packages/
│   ├── eslint-config/   # 共享 ESLint 配置
│   ├── prettier-config/ # 共享 Prettier 配置
│   └── typescript-config/ # 共享 TypeScript 配置
└── ...

清晰的项目结构让你可以轻松找到所需文件,也便于团队协作开发。

移动端适配方案

项目内置了完善的移动端适配方案,采用 px 到 vw 的自动转换,配合 viewport 设置,可以完美适配各种尺寸的移动设备。再也不用担心在不同手机上显示效果不一致的问题了。

社区共建

这是一个开源项目,作者非常欢迎各种形式的贡献,包括:

  • Bug修复
  • 新功能开发
  • 文档完善
  • 测试和反馈

你可以通过 Fork 项目并提交 Pull Request 的方式参与贡献,一起让这个项目变得更好。

结语

vite-vue3-h5-template 不仅是一个简单的模板,更是一套完整的移动端H5开发解决方案。它将复杂的工程化配置封装起来,让我们可以专注于业务开发,大大提升了开发效率和项目质量。

如果你正在寻找一个强大且易用的移动端H5开发模板,不妨试试这个项目。相信它会成为你开发路上的得力助手!

项目地址:github.com/wuxingxi888…

觉得不错的话,给项目点个star吧!也欢迎关注我的博客获取更多前端技术分享。

🚀 告别手动调试,Chrome DevTools MCP 推荐

作者 醒来明月
2025年10月29日 09:57

前言

作为一名前端开发者,你是否也曾经历过这样的场景:每次修改完代码都要手动打开浏览器刷新页面,检查功能是否正常、想要自动化测试,却被复杂的 Selenium 配置劝退、需要监控页面性能,却要在 DevTools 中手动操作等等。今天推荐一个可以改变作流程的 mcp 工具——Chrome DevTools MCP。这个工具让我们的 AI IDE(如 Cursor、Trae、VSCode 等)拥有了直接控制浏览器的"上帝视角"。

🎯 什么是 Chrome DevTools MCP?

简单来说,Chrome DevTools MCP 是一个连接 AI IDE 和 Chrome 浏览器的桥梁。它通过 Model Context Protocol(MCP)标准,让我们可以在 IDE 中直接控制浏览器的各种操作,就像我们手动使用 DevTools 一样,但这一切都是自动化的。

🔧 如何在IDE 中配置

在 Trae 中配置

  1. 打开 Trae 的设置面板
  2. 找到 "MCP 服务器" 配置项
  3. 添加 Chrome DevTools MCP 服务器:
    {
      "mcpServers": {
        "chrome-devtools": {
          "command": "npx",
          "args": ["-y", "chrome-devtools-mcp@latest"]
        }
      }
    }
    
  4. 给 Trae 操作浏览器的权限并重启。

在 VSCode 中配置

  1. 安装 "MCP Client" 扩展
  2. 在设置中添加:
    {
      "mcp.servers": {
        "chrome-devtools": {
          "command": "npx",
          "args": ["-y", "chrome-devtools-mcp@latest"]
        }
      }
    }
    
  3. 重新加载 VSCode。

配置完成后,你就可以在 IDE 中直接与浏览器对话了。

✨ 核心功能

1. 一键打开页面,告别手动操作

// 在 IDE 中直接输入
"帮我打开 https://example.com"

再也不用切换窗口,手动输入 URL 了,你的 AI 助手会直接为你打开页面,并返回页面快照。

2. 智能元素交互

// 点击按钮
"点击页面上的登录按钮"

// 填写表单
"在用户名输入框中填入 'admin',密码框中填入 'password123'"

// 拖拽操作
"把左侧的卡片拖拽到右侧的容器中"

这些操作比手动点击更精准,因为它直接通过 DOM 结构定位元素,不会因为 UI 变化而失效。

3. 性能分析,一键生成报告

性能分析是 Chrome DevTools MCP 中我最喜欢的功能,它能帮你快速定位性能瓶颈,比手动操作DevTools效率高出不知多少倍。

// 分析页面性能
"帮我分析这个页面的性能指标,特别是 LCP 和 CLS"

理解性能分析指标

性能分析不仅仅是获取几个数字,而是要理解每个指标背后的含义:

1. LCP (Largest Contentful Paint) - 最大内容绘制

// 帮我获取这个页面详细的LCP分解报告

// 结果示例:
// {
//   "lcp": 1240,           // LCP时间:1.24秒
//   "ttfb": 280,           // 首字节时间:280毫秒
//   "loadDelay": 450,      // 加载延迟:450毫秒
//   "renderDelay": 510     // 渲染延迟:510毫秒
// }

2. CLS (Cumulative Layout Shift) - 累积布局偏移

// 帮我分析页面布局稳定性

性能分析技巧

技巧1:对比分析

"帮我对比优化前后的性能数据,生成对比报告"

AI助手会自动进行A/B测试,生成直观的对比图表,让你一眼看出优化效果。

技巧2:资源加载分析

"分析页面资源加载情况,找出最耗时的请求"

自动识别阻塞渲染的资源、未压缩的文件、过大的图片等常见问题。

技巧3:运行时性能监控

"监控页面交互性能,特别是点击按钮后的响应时间"

跟踪用户交互到页面响应的完整链路,找出JavaScript执行瓶颈。

高级性能分析场景

场景1:首屏加载优化

"帮我分析首屏加载过程,找出可以优化的关键路径"

AI助手会:

  1. 启动性能跟踪并自动重载页面
  2. 分析关键渲染路径
  3. 识别阻塞资源
  4. 提供具体的优化建议

场景2:长列表性能优化

"分析这个长列表的滚动性能,找出卡顿原因"

自动检测:

  • 虚拟滚动实现是否正确
  • 事件监听器是否过多
  • DOM操作是否过于频繁
  • 内存泄漏风险

场景3:复杂交互性能分析

"分析这个复杂表单的提交性能,包括验证和提交过程"

跟踪从用户输入到服务器响应的完整流程,定位每一步的耗时。

性能问题自动诊断

Chrome DevTools MCP最强大的地方是能够自动诊断常见的性能问题:

"帮我诊断这个页面的性能问题,并给出具体的修复建议"

自动检测的问题包括:

  • 未优化的图片(格式、尺寸、压缩)
  • 渲染阻塞资源
  • 未使用的CSS和JavaScript
  • 第三方脚本性能影响
  • 内存泄漏风险
  • 过多的DOM节点

性能优化工作流

完整的工作流示例:

// 1. 基准测试
"先测试当前页面的性能基准,保存为baseline.json"

// 2. 实施优化
// ... 进行代码优化 ...

// 3. 验证优化效果
"重新测试性能,与baseline.json对比,生成优化报告"

// 4. 持续监控
"设置性能监控,如果LCP超过2秒立即通知"

这个完整的性能优化流程,从测试到优化再到监控,全部可以通过简单的自然语言指令完成。

性能分析最佳实践

  1. 建立性能基线:每次重大更改前先记录基准数据
  2. 分阶段测试:先测试核心页面,再扩展到全站
  3. 关注用户体验:不仅看数字,更要理解对用户的影响
  4. 持续监控:性能优化是一个持续的过程,不是一次性任务

通过这些强大的性能分析功能,你可以像性能专家一样快速定位和解决性能问题,而无需深入了解复杂的性能分析工具。

4. 自动截图

// 全页面截图
"截取整个页面的截图,保存为 performance-report.png"

// 元素截图
"截取登录按钮的截图"

💡 应用场景举例

场景一:自动化测试,让代码提交更安心

想象一下,你刚刚完成了一个复杂的功能,需要测试整个流程:

// 在 IDE 中输入这段话
"帮我测试一下登录流程:打开登录页面,输入用户名 'test@example.com' 和密码 'test123',点击登录按钮,等待页面跳转,然后截图确认登录成功"

几秒钟后,你的 AI 就会完成整个流程,并返回截图确认测试结果,再也不用手动一遍遍测试了。

场景二:性能监控,让网站速度一目了然

每次部署新版本后,都想确认性能没有退化:

"帮我监控首页性能,重点关注 LCP、FID 和 CLS 指标,并与上周的数据进行对比"

AI 会自动打开页面,收集性能数据,生成对比报告,甚至指出可能的性能问题。

场景三:数据爬取,让信息收集更高效

需要收集竞品网站的数据?再也不用写复杂的爬虫脚本了:

"帮我爬取这个产品列表页面的所有商品名称、价格和评分,保存为 JSON 文件"

AI 会自动打开页面,提取数据,甚至处理分页和动态加载的内容。

场景四:同时测试多个页面

"帮我同时打开首页、产品页和关于页,并行测试它们的加载性能"

AI 会创建多个浏览器标签页,并行执行测试,大大节省时间。

场景五:环境模拟,测试各种场景

"模拟慢速 3G 网络环境,测试页面加载性能"
"模拟移动设备视口,检查响应式布局"

一键切换测试环境,比手动配置 DevTools 简单很多。

🎊 结语

Chrome DevTools MCP 不仅提高了工作效率,更让开发者能够将更多精力集中在创造性的工作上,而不是重复性的测试和调试。如果你也在为重复的浏览器操作而烦恼,如果你也想让 AI IDE 更加强大,那么不妨试试 Chrome DevTools MCP 吧。

Subtitle Edit(字幕编辑软件) 中文绿色版

作者 非凡ghost
2025年10月29日 09:40

Subtitle Edit 是一款功能强大的免费字幕编辑软件,它支持多种字幕格式,包括 SRT、SSA、ASS、SUB、LRC、TXT 等常用格式,可以实现快速创建、编辑和同步字幕文件。

软件功能
  1. 支持多种字幕格式:SRT、SSA、ASS、SUB、LRC、TXT 等。
  2. 可以实时预览字幕效果,方便编辑和调整字幕。
  3. 支持批量处理字幕文件,快速完成字幕制作任务。
  4. 支持语音识别功能,可以将视频的音频转换成文本字幕。
  5. 支持多种翻译工具,可以进行实时翻译和字幕翻译。
  6. 支持字幕同步功能,可以根据视频的时间轴自动调整字幕时间点。
  7. 支持多种字体和样式选择,可以自定义字幕风格。
软件特点
  1. 界面简洁、操作简单,易于上手。
  2. 支持多种语言界面,包括中文、英文、日文等。
  3. 提供丰富的字幕编辑功能,包括剪切、复制、粘贴、删除、移动等。
  4. 支持多种视频格式,包括 AVI、MP4、MKV、WMV 等。
  5. 支持多种字幕语言,包括中文、英文、法文、西班牙文等。
  6. 提供多种字幕效果,包括字体大小、颜色、样式、阴影、描边等。
  7. 支持字幕翻译功能,可以根据用户需求自动翻译字幕。
    总之,Subtitle Edit 是一款功能强大、易于操作的字幕编辑软件,可以帮助用户快速创建、编辑和同步字幕文件,是制作字幕的不错选择。
中文设置

Options – Choose Language… – 中文简体 – OK。

「Subtitle Edit(字幕编辑软件) v4.0.14 中文绿色版」 链接:pan.quark.cn/s/7c01467e1…

从 0 到 1,我用小程序 + 云开发打造了一个“记忆瓶子”,记录那些重要的日子!

2025年10月29日 09:26

缘起:为什么需要一个“记忆瓶子”?

市面上有很多纪念日 APP,但它们常常伴随着各种广告弹窗、冗余功能,或者在UI设计上未能满足我个人对于“简洁与美”的追求。我希望能有一个小而美的应用,能够:

  • 纯粹地记录重要的公历或农历纪念日。
  • 能够自由设置重复提醒。
  • 界面美观,甚至可以自定义背景图。
  • 最重要的是,没有多余的干扰,安静地守护那些珍贵的回忆。

于是,“记忆瓶子”的构思便在脑海中逐渐成形。在深入技术细节之前,欢迎扫码快速体验‘记忆瓶子’! gh\_6ba58d08bd84\_344.jpg

技术选型:原生小程序 + 云开发的“双剑合璧”

作为一名开发者,选择合适的技术栈是项目成功的关键。

  • 微信小程序: 无疑是触达用户最便捷的平台之一。其轻量级、无需下载安装的特性,以及微信生态内丰富的接口能力(订阅消息、用户授权等),都让它成为首选。
  • 微信云开发: 这次我选择了“All in 云开发”的模式。云开发提供了数据库、云存储、云函数等一站式服务,大大降低了后端开发和运维的复杂度。对于个人开发者而言,免费额度友好,上手成本极低,能够让我们更专注于前端和业务逻辑的实现。

这套组合让我在短时间内能够快速迭代,将产品想法落地。

核心功能与技术亮点

1. 公农历转换与复杂日期计算的“艺术”

“纪念日”的核心就是日期。但它远非简单的加减法,公历、农历、重复、指定年数,甚至还有时区问题,都让日期计算变得异常复杂。

痛点: 用户可能想记录一个农历生日,每年自动提醒;或者一个固定公历的周年纪念日;又或者是一个只发生一次的特殊事件。如何精准地计算出这些纪念日的“下一个发生日期”,并与当前日期进行比对,是小程序的核心挑战。 解决方案: 我引入了两个强大的日期处理库:

  • solarlunar: 用于精准地进行公历和农历之间的转换。尤其是在处理闰月等复杂情况时,它的表现非常可靠。
  • dayjs 及其 utc/timezone 插件: 这两个插件对于处理跨时区(尤其是中国大陆)的日期计算至关重要。我发现,仅仅使用 new Date() 可能会在服务器和用户手机之间产生时区差异,导致“今天”的判断不准确。通过 dayjs().tz('Asia/Shanghai').startOf('day') 能够确保在云函数和前端都以统一的“上海时间”零点作为基准,从而确保倒计时和提醒的精确性。

核心逻辑片段(getList 云函数中计算 nextOccurrenceTimestamp 的部分简化版):

// 假设 item.timestamp 是事件的UTC时间戳
const eventDayjsUTC = dayjs(item.timestamp);
const eventInTimeZone = eventDayjsUTC.tz(TARGET_TIMEZONE); // TARGET_TIMEZONE = 'Asia/Shanghai'
const todayStartInTimeZone = dayjs().tz(TARGET_TIMEZONE).startOf('day');

let nextSolarYear, nextSolarMonth, nextSolarDay;

if (item.dateType === 'lunar' && item.isRepeat) {
    // 农历重复纪念日的复杂计算...
    let nextSolar = solarlunar.lunar2solar(todayStartInTimeZone.year(), item.lunarMonth, item.lunarDay, item.isLeapMonth || false);
    // ... 如果今年已过,则计算明年的 ...
    if (dayjs.tz(`${nextSolar.year}-${nextSolar.month}-${nextSolar.day}`, TARGET_TIMEZONE).valueOf() < todayStartInTimeZone.valueOf()) {
        nextSolar = solarlunar.lunar2solar(todayStartInTimeZone.year() + 1, item.lunarMonth, item.lunarDay, item.isLeapMonth || false);
    }
    nextSolarYear = nextSolar.year;
    nextSolarMonth = nextSolar.month;
    nextSolarDay = nextSolar.day;

} else if (item.dateType === 'solar' && item.isRepeat) {
    // 公历重复纪念日的计算...
    nextSolarYear = todayStartInTimeZone.year();
    nextSolarMonth = eventInTimeZone.month() + 1;
    nextSolarDay = eventInTimeZone.date();
    // ... 如果今年已过,则计算明年的 ...
    if (dayjs.tz(`${nextSolarYear}-${nextSolarMonth}-${nextSolarDay}`, TARGET_TIMEZONE).valueOf() < todayStartInTimeZone.valueOf()) {
        nextSolarYear++;
    }
} else {
    // 一次性纪念日的计算...
    nextSolarYear = eventInTimeZone.year();
    nextSolarMonth = eventInTimeZone.month() + 1;
    nextSolarDay = eventInTimeZone.date();
}

// 最终构建下一个事件的 Day.js 对象
const nextEventDateStr = `${nextSolarYear}-${String(nextSolarMonth).padStart(2,'0')}-${String(nextSolarDay).padStart(2,'0')}`;
const nextEventStartInTimeZone = dayjs.tz(nextEventDateStr, TARGET_TIMEZONE);
const nextOccurrenceTimestamp = nextEventStartInTimeZone.valueOf();

// ... 剩余的倒计时计算 ...

订阅消息:不错过每一个重要提醒

纪念日如果没有提醒,就失去了意义。“记忆瓶子”集成了微信小程序的订阅消息功能。

实现:

  • 用户在小程序内通过 wx.requestSubscribeMessage 授权接收提醒。
  • 我部署了一个定时触发的云函数 checkReminders。该云函数会每天运行,遍历所有用户的纪念日,计算每个纪念日的目标提醒日期(即“下一个发生日期”减去“提前提醒天数”)。
  • 如果目标提醒日期恰好是今天,云函数便会调用 cloud.openapi.subscribeMessage.send 发送订阅消息。
  • 踩坑提示: 在发送订阅消息时,有一个关键参数 miniprogramState初期为了调试方便,我将其设为 'developer',导致用户点击消息后跳转到开发版小程序! 正式上线后,务必将其改为 'formal',确保用户跳转到正式发布版。

checkReminders 云函数核心片段:

// 假设 nextOccurrenceTimestamp 已经计算好
const targetReminderTimestamp = nextOccurrenceTimestamp - (reminderDaysBefore * ONE_DAY_MS);

if (targetReminderTimestamp === todayTimestamp) { // todayTimestamp 是目标时区今天的0点时间戳
    // 构建消息数据
    const messageData = { /* ... */ };
    remindersToSend.push({
        touser: item._openid,
        templateId: REMINDER_TEMPLATE_ID,
        page: `pages/detail/index?id=${item._id}`,
        data: messageData,
        miniprogramState: 'formal' // 【关键】确保跳转到正式版!
    });
}

云存储与 Base64 图片上传:打造个性化背景

为了让每个纪念日卡片都能拥有独特的视觉效果,我引入了自定义背景图功能。然而,云开发免费版存储空间有限,直接上传大量图片并不是最优解。

巧妙的解决方案:

  • 用户选择图片后,小程序端会先对图片进行压缩处理(减少数据量)。
  • 接着,将压缩后的图片数据转为 Base64 编码字符串
  • 这个 Base64 字符串会被直接作为纪念日记录的一个字段,存储在云数据库中(而非云存储)。
  • 前端渲染时,直接将这个 Base64 字符串赋值给 <img> 标签的 src 属性,图片便能直接显示。

【好处】 : 这种方式巧妙地绕过了云存储的容量限制,对于图片数量不多的个人应用来说,大大节约了成本,并且部署简单,加载速度也很快。

UI/UX 优化与样式攻坚战:Vant Weapp 的“爱恨交织”

为了快速构建美观的界面,我选择了 Vant Weapp 组件库。它提供了丰富的组件和完善的文档,确实提升了开发效率。然而,在详情页的底部操作按钮布局上,我遭遇了一场漫长而痛苦的“攻坚战”!

问题描述: 在详情页,我希望“编辑”和“删除”两个按钮能够并排显示,且平分底部空间,左右留出相等的内边距。在开发者工具模拟器上,通过 display: flex; gap: 24rpx; 配合 <van-button custom-class="action-button" />.action-button { flex: 1; min-width: 0; box-sizing: border-box; } 似乎完美解决了。然而,在真机上,按钮的布局却始终是“左边有空白,右边没有”,呈现出不均衡甚至轻微溢出的情况!

排查过程:

  • 检查 flex 容器和子项的 padding, margin, box-sizing
  • 尝试使用 calc() 函数精确计算宽度。
  • 利用真机调试功能,我发现罪魁祸首竟然是微信小程序底层对原生 button 注入的默认样式wx-button:not([size=mini]) { width: 184px; ...; margin-left: auto; margin-right: auto; }。这个固定宽度和 auto 外边距在 Vant Weapp 复杂的组件结构内部,以某种难以覆盖的方式生效,破坏了我的 flex: 1 布局!

最终解决方案: 在尝试了各种 !important 覆盖、多层选择器、甚至修改 Vant 内部样式(失败告终)之后,我做出了一个决定:放弃使用 van-button 来实现底部操作按钮

我选择用两个原生的 <view> 标签来模拟按钮:

<view class="action-button-wrapper">
  <view class="custom-action-button custom-edit-button" hover-class="button-hover" bindtap="onEdit">
    <text>编 辑</text>
  </view>
  <view class="custom-action-button custom-delete-button" hover-class="button-hover" bindtap="onDelete">
    <text>删 除</text>
  </view>
</view>

并结合以下 CSS 样式:

.action-button-wrapper {
  display: flex;
  gap: 24rpx;
  padding: 40rpx 24rpx;
  box-sizing: border-box;
  width: 100%;
}
.custom-action-button {
  flex: 1;
  min-width: 0;
  box-sizing: border-box;
  height: 80rpx;
  border-radius: 40rpx;
  font-size: 28rpx;
  font-weight: bold;
  color: #FFFFFF;
  text-align: center;
  line-height: 80rpx;
}
.custom-edit-button { background-color: #C8B6B6; }
.custom-delete-button { background-color: #ee0a24; }
.button-hover { opacity: 0.85; }

这套方案简单、直接,让我对样式有了 100% 的掌控力,最终在真机上完美实现了预期布局。有时候,回归原生是最可靠的解决方案!

版本更新机制:确保用户始终使用最新功能

为了避免用户因为小程序缓存而无法体验最新功能或修复的 Bug,我在 app.js 中集成了 wx.getUpdateManager()。它能检测到小程序新版本下载完成后,弹窗提示用户重启。

关键点:

  • 监听器 onUpdateReady 必须在 onLaunch 时就设置好。
  • 【非常重要】 每次上传新版本前,务必手动修改项目根目录下 project.config.json 文件中的 "version" 字段(如从 1.0.0 改为 1.1.0),否则微信后台无法识别为新版本,更新机制也无法触发。

产品未来展望

“记忆瓶子”才刚刚起步,未来还有很多有趣的功能等待探索:

  • 共享空间: 允许多人(如情侣、家人)共同创建和维护纪念日,分享彼此的珍贵时刻。
  • 更多主题与精美分享: 提供更丰富的 UI 主题、背景素材,并支持一键生成精美的分享图片。
  • 年度总结与历史上的今天: 增加趣味性功能,回顾一年的点滴,或发现“历史上的今天”发生了什么。

总结

开发“记忆瓶子”的过程,是一个不断学习、不断解决问题的过程。它让我对小程序和云开发有了更深的理解,也再次体会到作为一名开发者,能够将自己的想法变为现实的乐趣。

如果你也对这个小程序感兴趣,欢迎扫码体验,并留下宝贵的反馈和建议!你的每一次使用和反馈,都将是“记忆瓶子”继续成长的动力。再次邀请体验和反馈

gh\_6ba58d08bd84\_344.jpg

感谢阅读!

🐍 前端开发 0 基础学 Python 入门指南:条件语句篇

作者 王六岁
2025年10月29日 09:24

🐍 前端开发 0 基础学 Python 入门指南:条件语句篇

从 JavaScript 到 Python,深入理解 if/else 条件判断的差异!

📝 一、Python 中的 if 语句基础

1.1 基本语法对比

Python 和 JavaScript 的 if 语句语法有明显差异:

# Python - 使用冒号和缩进
age = 18
if age >= 18:
    print("你已经成年了")
else:
    print("你还未成年")
// JavaScript - 使用花括号
let age = 18
if (age >= 18) {
  console.log('你已经成年了')
} else {
  console.log('你还未成年')
}

1.2 关键语法差异

特性 Python JavaScript 说明
条件后符号 冒号 : 无(直接用 {} Python 必须要冒号
代码块 缩进 花括号 {} Python 用缩进表示层级
条件括号 不需要括号 需要括号 () Python 更简洁
缩进规范 强制(通常 4 空格) 可选(仅为美观) Python 缩进错误会报错!
elif elif else if Python 更简短

⚠️ 重要提示:Python 的缩进不是装饰,而是语法的一部分!

# ✅ 正确 - 使用4个空格缩进
if age >= 18:
    print("成年人")

# ❌ 错误 - 缩进不一致会报错
if age >= 18:
    print("成年人")
  print("可以投票")  # IndentationError!

🔄 二、if-elif-else 多重条件

2.1 成绩评级示例

# Python
score = 85

if score >= 90:
    print("优秀")
elif score >= 75:
    print("良好")
elif score >= 60:
    print("及格")
else:
    print("不及格")
# 输出: 良好
// JavaScript
let score = 85

if (score >= 90) {
  console.log('优秀')
} else if (score >= 75) {
  console.log('良好')
} else if (score >= 60) {
  console.log('及格')
} else {
  console.log('不及格')
}
// 输出: 良好

2.2 执行流程

score = 85
    ↓
score >= 90?  → ❌ 否
    ↓
score >= 75?  → ✅ 是 → 打印"良好" → 结束
    ↓
(不再检查后续条件)

关键点: 找到第一个满足的条件后,立即执行并结束,不会检查后续条件!


🎯 三、多重条件判断

3.1 逻辑运算符

Python 和 JavaScript 的逻辑运算符有所不同:

逻辑 Python JavaScript 说明
and && 都为真
or || 至少一个真
not ! 取反

3.2 判断正负奇偶

# Python
num = 15

if num > 0 and num % 2 == 0:
    print("正偶数")
elif num > 0 and num % 2 != 0:
    print("正奇数")
elif num < 0 and num % 2 == 0:
    print("负偶数")
elif num < 0 and num % 2 != 0:
    print("负奇数")
else:
    print("零")
# 输出: 正奇数
// JavaScript
let num = 15

if (num > 0 && num % 2 === 0) {
  console.log('正偶数')
} else if (num > 0 && num % 2 !== 0) {
  console.log('正奇数')
} else if (num < 0 && num % 2 === 0) {
  console.log('负偶数')
} else if (num < 0 && num % 2 !== 0) {
  console.log('负奇数')
} else {
  console.log('零')
}
// 输出: 正奇数

3.3 逻辑运算符优先级

# and 优先级高于 or
result = True or False and False
print(result)  # True
# 等价于: True or (False and False)

# 使用括号明确优先级(推荐✅)
result = (True or False) and False
print(result)  # False

🔀 四、嵌套 if 语句

4.1 闰年判断

# Python
year = 2024

if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0):
    print(f"{year} 是闰年")
else:
    print(f"{year} 不是闰年")
# 输出: 2024 是闰年

闰年规则:

  1. 能被 4 整除 不能被 100 整除
  2. 或者 能被 400 整除
# 嵌套写法(更清晰)
year = 2024

if year % 400 == 0:
    print(f"{year} 是闰年")
elif year % 100 == 0:
    print(f"{year} 不是闰年")
elif year % 4 == 0:
    print(f"{year} 是闰年")
else:
    print(f"{year} 不是闰年")

4.2 嵌套层级对比

# Python - 用缩进表示层级
age = 20
has_license = True

if age >= 18:
    if has_license:
        print("可以开车")
    else:
        print("需要考驾照")
else:
    print("未成年,不能开车")
// JavaScript - 用花括号表示层级
let age = 20
let hasLicense = true

if (age >= 18) {
  if (hasLicense) {
    console.log('可以开车')
  } else {
    console.log('需要考驾照')
  }
} else {
  console.log('未成年,不能开车')
}

⚠️ 避免过深嵌套: 嵌套超过 3 层会降低代码可读性,建议重构!


📊 五、if 语句与数据结构

5.1 列表过滤 - 筛选偶数

# Python
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_numbers = []

for num in numbers:
    if num % 2 == 0:
        even_numbers.append(num)

print("偶数列表:", even_numbers)
# 输出: 偶数列表: [2, 4, 6, 8, 10]
// JavaScript
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let evenNumbers = []

for (let num of numbers) {
  if (num % 2 === 0) {
    evenNumbers.push(num)
  }
}

console.log('偶数列表:', evenNumbers)
// 输出: 偶数列表: [2, 4, 6, 8, 10]

5.2 列表推导式(Python 独有 ✨)

Python 提供了更简洁的语法:

# 方式1: 传统 for + if
even_numbers = []
for num in numbers:
    if num % 2 == 0:
        even_numbers.append(num)

# 方式2: 列表推导式(推荐✅)
even_numbers = [num for num in numbers if num % 2 == 0]
print(even_numbers)  # [2, 4, 6, 8, 10]

# 更多示例
squares = [x**2 for x in range(1, 6)]  # [1, 4, 9, 16, 25]
positive = [x for x in [-2, -1, 0, 1, 2] if x > 0]  # [1, 2]

JavaScript 需要使用 filter 方法:

// JavaScript
let evenNumbers = numbers.filter((num) => num % 2 === 0)
console.log(evenNumbers) // [2, 4, 6, 8, 10]

5.3 字典过滤 - 筛选及格学生

# Python
students = {'Alice': 85, 'Bob': 92, 'Charlie': 78, 'David': 90}
passed_students = {}

for name, score in students.items():
    if score >= 80:
        passed_students[name] = score

print("及格学生:", passed_students)
# 输出: 及格学生: {'Alice': 85, 'Bob': 92, 'David': 90}

使用字典推导式(更简洁 ✨):

# 字典推导式
passed_students = {name: score for name, score in students.items() if score >= 80}
print(passed_students)
# 输出: {'Alice': 85, 'Bob': 92, 'David': 90}

JavaScript 对比:

// JavaScript
let students = { Alice: 85, Bob: 92, Charlie: 78, David: 90 }
let passedStudents = {}

for (let [name, score] of Object.entries(students)) {
  if (score >= 80) {
    passedStudents[name] = score
  }
}

console.log('及格学生:', passedStudents)

// 或使用 reduce
let passedStudents = Object.entries(students).reduce((acc, [name, score]) => {
  if (score >= 80) acc[name] = score
  return acc
}, {})

🎨 六、真值判断(Truthy/Falsy)

6.1 Python 的假值

Python 中以下值被视为 False

# 布尔值
bool_val = False

# 数字零
zero_int = 0
zero_float = 0.0
zero_complex = 0j

# 空序列
empty_str = ""
empty_list = []
empty_tuple = ()

# 空映射
empty_dict = {}
empty_set = set()

# None
none_val = None

# 测试
if not empty_list:
    print("空列表为假")  # ✅ 会执行

6.2 真值判断示例

# 判断字符串是否为空
name = ""
if name:
    print(f"你好, {name}")
else:
    print("名字不能为空")  # ✅ 会执行

# 判断列表是否有元素
items = []
if items:
    print(f"有 {len(items)} 个商品")
else:
    print("购物车是空的")  # ✅ 会执行

# 判断字典是否有数据
user = {}
if user:
    print(f"用户: {user['name']}")
else:
    print("未登录")  # ✅ 会执行

6.3 Python vs JavaScript 假值对比

假值类型 Python JavaScript 说明
布尔 False false 相同
数字零 0, 0.0 0, -0, 0n 相同
空字符串 "" "" 相同
空值 None null, undefined Python 只有 None
空数组 [] [] 是真值! 不同
空对象 {} {} 是真值! 不同
NaN ❌ 无 NaN JS 独有

关键区别:

# Python - 空列表/字典为假
if []:
    print("不会执行")
// JavaScript - 空数组/对象为真!⚠️
if ([]) {
  console.log('会执行!') // ✅
}

if ({}) {
  console.log('会执行!') // ✅
}

// JS 需要显式判断
if (arr.length > 0) {
  console.log('数组有元素')
}

🔍 七、三元运算符(条件表达式)

7.1 基本语法

# Python - 条件表达式
age = 20
status = "成年" if age >= 18 else "未成年"
print(status)  # "成年"

# 等价于
if age >= 18:
    status = "成年"
else:
    status = "未成年"
// JavaScript - 三元运算符
let age = 20
let status = age >= 18 ? '成年' : '未成年'
console.log(status) // "成年"

7.2 语法对比

特性 Python JavaScript
语法 值1 if 条件 else 值2 条件 ? 值1 : 值2
顺序 条件在中间 条件在前面
可读性 更接近自然语言 符号化

7.3 嵌套三元运算符

# Python
score = 85
grade = "优秀" if score >= 90 else "良好" if score >= 75 else "及格" if score >= 60 else "不及格"
print(grade)  # "良好"

# ⚠️ 建议:嵌套过多时使用 if-elif-else 更清晰
if score >= 90:
    grade = "优秀"
elif score >= 75:
    grade = "良好"
elif score >= 60:
    grade = "及格"
else:
    grade = "不及格"
// JavaScript
let score = 85
let grade =
  score >= 90 ? '优秀' : score >= 75 ? '良好' : score >= 60 ? '及格' : '不及格'
console.log(grade) // "良好"

🚀 八、高级技巧

8.1 in 运算符判断

# 判断元素是否在序列中
fruit = "apple"
if fruit in ["apple", "banana", "orange"]:
    print("这是水果")  # ✅ 会执行

# 判断键是否在字典中
user = {"name": "张三", "age": 25}
if "name" in user:
    print(f"用户名: {user['name']}")  # ✅ 会执行

# 判断子串是否在字符串中
text = "Hello World"
if "World" in text:
    print("包含 World")  # ✅ 会执行

JavaScript 对比:

// JavaScript
let fruit = 'apple'
if (['apple', 'banana', 'orange'].includes(fruit)) {
  console.log('这是水果')
}

let user = { name: '张三', age: 25 }
if ('name' in user) {
  console.log(`用户名: ${user.name}`)
}

let text = 'Hello World'
if (text.includes('World')) {
  console.log('包含 World')
}

8.2 match 语句(Python 3.10+)

Python 3.10 引入了类似 switch 的语法:

# Python 3.10+
status_code = 404

match status_code:
    case 200:
        print("请求成功")
    case 404:
        print("页面未找到")  # ✅ 会执行
    case 500:
        print("服务器错误")
    case _:
        print("未知状态")

# 支持模式匹配
point = (0, 0)
match point:
    case (0, 0):
        print("原点")  # ✅ 会执行
    case (0, y):
        print(f"y 轴上的点: y={y}")
    case (x, 0):
        print(f"x 轴上的点: x={x}")
    case (x, y):
        print(f"坐标: ({x}, {y})")

JavaScript switch 语句:

// JavaScript
let statusCode = 404

switch (statusCode) {
  case 200:
    console.log('请求成功')
    break
  case 404:
    console.log('页面未找到') // ✅ 会执行
    break
  case 500:
    console.log('服务器错误')
    break
  default:
    console.log('未知状态')
}

关键区别: Python 的 match 不需要 break,更强大的模式匹配!

8.3 海象运算符(:=)Python 3.8+

在条件表达式中赋值:

# Python 3.8+
# 传统写法
user_input = input("请输入: ")
if len(user_input) > 5:
    print(f"输入太长: {len(user_input)} 个字符")

# 使用海象运算符
if (n := len(input("请输入: "))) > 5:
    print(f"输入太长: {n} 个字符")

# 实用场景:避免重复计算
import re
text = "联系电话: 138-0013-8000"
if (match := re.search(r'\d{3}-\d{4}-\d{4}', text)):
    print(f"找到电话: {match.group()}")

🆚 九、总结对比

9.1 语法差异总结

特性 Python JavaScript
条件后符号 冒号 :
代码块 缩进(4 空格) 花括号 {}
条件括号 不需要 需要 ()
elif elif else if
逻辑与 and &&
逻辑或 or ||
逻辑非 not !
三元运算符 值1 if 条件 else 值2 条件 ? 值1 : 值2
Switch match(3.10+) switch
成员判断 in .includes() / in
空值 None null, undefined

9.2 假值差异

类型 Python JavaScript
空数组 ❌ 假 ✅ 真
空对象 ❌ 假 ✅ 真
空字符串 ✅ 假 ✅ 假
数字零 ✅ 假 ✅ 假

9.3 核心差异

特性 Python JavaScript
代码风格 缩进强制 花括号
语法设计 接近自然语言 符号化
列表推导式 ✅ 原生支持 ❌ 需用 filter/map
match 语句 ✅ 强大的模式匹配 switch 较简单
空容器 视为假值 视为真值

💡 最佳实践

10.1 保持正确的缩进

# ✅ 好 - 使用4个空格
if condition:
    do_something()
    do_another()

# ❌ 不好 - 混用空格和Tab
if condition:
    do_something()
do_another()  # 可能导致错误!

提示: VS Code 可设置自动转换 Tab 为空格。

10.2 避免过深嵌套

# ❌ 不好 - 嵌套太深
if condition1:
    if condition2:
        if condition3:
            if condition4:
                do_something()

# ✅ 好 - 提前返回
if not condition1:
    return
if not condition2:
    return
if not condition3:
    return
if not condition4:
    return
do_something()

10.3 利用 Python 的真值判断

# ❌ 不好
if len(items) > 0:
    process(items)

# ✅ 好 - 更 Pythonic
if items:
    process(items)

# ❌ 不好
if user_name != "":
    greet(user_name)

# ✅ 好
if user_name:
    greet(user_name)

10.4 使用 in 简化判断

# ❌ 不好
if status == 200 or status == 201 or status == 204:
    print("成功")

# ✅ 好
if status in [200, 201, 204]:
    print("成功")

# 或使用元组(更快)
if status in (200, 201, 204):
    print("成功")

10.5 复杂条件使用变量

# ❌ 不好 - 条件复杂难读
if user.age >= 18 and user.has_license and not user.is_banned:
    allow_drive()

# ✅ 好 - 使用有意义的变量名
is_adult = user.age >= 18
has_permission = user.has_license and not user.is_banned

if is_adult and has_permission:
    allow_drive()

🎯 实战练习

练习 1: BMI 计算器

print("=== BMI 健康指数计算器 ===\n")

# 输入身高和体重
height = float(input("请输入身高(米): "))
weight = float(input("请输入体重(公斤): "))

# 计算 BMI
bmi = weight / (height ** 2)

# 判断健康状态
print(f"\n你的 BMI 指数是: {bmi:.2f}")

if bmi < 18.5:
    status = "偏瘦"
    suggestion = "建议增加营养摄入"
elif bmi < 24:
    status = "正常"
    suggestion = "继续保持健康生活方式"
elif bmi < 28:
    status = "偏胖"
    suggestion = "建议适量运动和控制饮食"
else:
    status = "肥胖"
    suggestion = "建议咨询医生并制定减重计划"

print(f"健康状态: {status}")
print(f"建议: {suggestion}")

练习 2: 会员折扣系统

print("=== 购物折扣计算系统 ===\n")

# 输入信息
total_amount = float(input("购物金额: "))
is_member = input("是否会员? (是/否): ") == "是"
member_years = 0

if is_member:
    member_years = int(input("会员年限: "))

# 计算折扣
discount = 0

if not is_member:
    # 非会员: 满200打9折
    if total_amount >= 200:
        discount = 0.1
else:
    # 会员折扣
    if member_years >= 5:
        discount = 0.3  # 5年以上会员: 7折
    elif member_years >= 2:
        discount = 0.2  # 2-5年会员: 8折
    else:
        discount = 0.15  # 新会员: 85折

    # 额外满减
    if total_amount >= 500:
        discount += 0.05  # 额外5%折扣

# 计算最终价格
discount = min(discount, 0.5)  # 最多5折
final_amount = total_amount * (1 - discount)
saved = total_amount - final_amount

# 输出结果
print("\n" + "=" * 40)
print(f"原价: ¥{total_amount:.2f}")
print(f"折扣: {discount * 100:.0f}%")
print(f"应付: ¥{final_amount:.2f}")
print(f"节省: ¥{saved:.2f}")
print("=" * 40)

练习 3: 密码强度检测

def check_password_strength(password):
    """检测密码强度"""

    # 检查长度
    length = len(password)

    # 检查各种字符类型
    has_lower = any(c.islower() for c in password)
    has_upper = any(c.isupper() for c in password)
    has_digit = any(c.isdigit() for c in password)
    has_special = any(not c.isalnum() for c in password)

    # 计算得分
    score = 0

    if length >= 8:
        score += 1
    if length >= 12:
        score += 1
    if has_lower:
        score += 1
    if has_upper:
        score += 1
    if has_digit:
        score += 1
    if has_special:
        score += 1

    # 判断强度
    if score <= 2:
        strength = "弱"
        color = "🔴"
    elif score <= 4:
        strength = "中"
        color = "🟡"
    else:
        strength = "强"
        color = "🟢"

    # 输出建议
    suggestions = []
    if length < 8:
        suggestions.append("密码长度至少8位")
    if not has_lower:
        suggestions.append("添加小写字母")
    if not has_upper:
        suggestions.append("添加大写字母")
    if not has_digit:
        suggestions.append("添加数字")
    if not has_special:
        suggestions.append("添加特殊字符")

    return strength, color, score, suggestions

# 测试
print("=== 密码强度检测器 ===\n")
password = input("请输入密码: ")

strength, color, score, suggestions = check_password_strength(password)

print(f"\n密码强度: {color} {strength} (得分: {score}/6)")

if suggestions:
    print("\n改进建议:")
    for i, tip in enumerate(suggestions, 1):
        print(f"  {i}. {tip}")
else:
    print("\n✅ 密码强度很好!")

📚 核心要点

  1. 语法特点

    • Python 使用 冒号 + 缩进,不用花括号
    • 缩进是语法的一部分,错误会导致报错
    • elif 代替 else if
  2. 逻辑运算符

    • 使用 and, or, not(不是 &&, ||, !
    • 更接近自然语言,易读
  3. 真值判断

    • 空容器(列表、字典、字符串)为假
    • 与 JavaScript 不同:[]{} 在 Python 中是假值
  4. 高级特性

    • 列表/字典推导式:简洁的过滤和转换
    • in 运算符:优雅的成员判断
    • match 语句:强大的模式匹配(3.10+)
    • 海象运算符:在条件中赋值(3.8+)
  5. 最佳实践

    • 保持一致的缩进(4 空格)
    • 避免过深嵌套,提前返回
    • 利用 Python 的真值判断特性
    • 使用有意义的变量名提高可读性
  6. 关键区别

    • Python: 强制缩进、更 Pythonic 的写法
    • JavaScript: 花括号、更灵活但易出错

本文适合前端开发者快速掌握 Python 的条件语句,通过大量实例和对比理解 Python 的特点。

10个JavaScript编程实用技巧

作者 w2sfot
2025年10月29日 09:13

JavaScript编程10个实用技巧

1. 箭头函数简化

// 传统写法
setTimeout(function() {
    console.log('Hello');
}, 1000);

// 箭头函数
setTimeout(() => console.log('Hello'), 1000);

2. 解构赋值

const config = {apiUrl: 'https://api.example.com', timeout: 5000};
const {apiUrl, timeout} = config;

3. 模板字符串

const name = '李四';
console.log(`欢迎 ${name} 使用我们的服务!`);

4. 可选链操作符

// 安全访问嵌套属性
const userCity = user?.address?.city ?? '未知';

5. 空值合并

const port = process.env.PORT ?? 3000;

6. 展开运算符

const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5]; // [1,2,3,4,5]

7. Promise简化

// 使用async/await替代.then()
async function fetchData() {
    try {
        const data = await fetch('/api/data');
        return data.json();
    } catch (error) {
        console.error('请求失败', error);
    }
}

8. 数组方法链式调用

const result = array
    .filter(item => item.active)
    .map(item => item.name)
    .slice(0, 10);

9. 短路求值

// 条件赋值
const isAdmin = user.role === 'admin';
isAdmin && showAdminPanel();

10. 事件委托

// 避免为每个子元素绑定事件
document.getElementById('list').addEventListener('click', (e) => {
    if (e.target.tagName === 'LI') {
        console.log('点击了:', e.target.textContent);
    }
});

掌握这些实用技巧能让你的JavaScript代码更加简洁、高效且易于维护。

另外,值得注意的是:JavaScript代码是公开透明的代码,容易在执行环境中被查看、复制、盗用,如果有重要的代码,建议使用JShaman进行JS代码混淆加密,使代码变的不可读、不可分析,以保护代码安全。

扩展卡片效果:用 Flexbox 和 CSS 过渡打造惊艳交互体验

作者 San30
2025年10月29日 01:07

前言

本文将带你使用纯 CSS 和少量 JavaScript 实现一个流行的扩展卡片效果,探索 Flexbox 布局和 CSS 过渡动画的强大能力。

效果预览

这是一个优雅的图片展示组件,包含多个卡片面板。当用户点击某个面板时,该面板会平滑扩展并显示标题,其他面板则相应收缩。这种交互方式不仅美观,还能有效吸引用户的注意力。

HTML 结构设计

首先,我们来看 HTML 结构的设计:

<div class="container">
    <div class="panel" style="background-image: url('image1.jpg')">
        <h3>Explore The World</h3>
    </div>
    <div class="panel" style="background-image: url('image2.jpg')">
        <h3>Wild Forest</h3>
    </div>
    <!-- 更多面板... -->
</div>

设计思路

  • 使用容器包裹所有面板,便于整体布局控制
  • 每个面板通过内联样式设置背景图片,保持灵活性
  • 标题使用 <h3> 标签,语义化且样式可控
  • 结构简洁,便于维护和扩展

CSS 核心技术解析

1. 基础重置与整体布局

* {
    margin: 0;
    padding: 0;
}

body {
    display: flex;
    flex-direction: row;
    justify-content: center;
    align-items: center;
    height: 100vh;
    overflow: hidden;
}

关键技术点

  • 通配符重置消除浏览器默认样式差异
  • display: flex 让 body 成为弹性容器,便于居中布局
  • height: 100vh 确保布局充满整个视口高度
  • overflow: hidden 防止滚动条出现

2. 弹性容器设置

.container {
    display: flex;
    width: 90vw;
}

这里将容器设置为 Flex 布局,width: 90vw 让容器宽度为视口宽度的 90%,留出适当的边距。

3. 面板基础样式

.panel {
    height: 80vh;
    border-radius: 50px;
    color: #fff;
    cursor: pointer;
    flex: 0.5;
    margin: 10px;
    position: relative;
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    transition: all 700ms ease-in;
}

样式解析

  • height: 80vh - 相对视口高度,保持响应式
  • border-radius: 50px - 圆角设计,现代感强
  • flex: 0.5 - 初始状态下每个面板占据较小空间
  • position: relative - 为绝对定位的标题做准备
  • 背景相关属性确保图片完美显示
  • transition: all 700ms ease-in - 平滑的过渡动画

4. 标题样式与动画

.panel h3 {
    font-size: 24px;
    position: absolute;
    left: 20px;
    bottom: 20px;
    margin: 0;
    opacity: 0;
    transition: opacity 300ms ease-in 400ms;
}

动画设计

  • opacity: 0: 初始状态隐藏
  • 独立的过渡动画:opacity 300ms ease-in 400ms
  • 400ms 延迟让面板扩展完成后再显示文字

5. 激活状态样式

.panel.active {
    flex: 5;
}

.panel.active h3 {
    opacity: 1;
}

激活状态时:

  • flex: 5:面板扩展占据更多空间
  • opacity: 1: 标题透明度变为1,平滑显示

6. 响应式设计

@media (max-width: 480px) {
    .container {
        width: 100vw;
    }
    .panel:nth-of-type(4),
    .panel:nth-of-type(5) {
        display: none;
    }
}

移动端适配

  • 小屏幕下隐藏最后两个面板,保持可用性
  • 容器宽度调整为 100%,充分利用空间

JavaScript 交互逻辑

const panels = document.querySelectorAll(".panel");

panels.forEach(function(panel) {
    panel.addEventListener("click", function() {
        panel.classList.toggle("active");
    })
})

交互逻辑

  • 使用事件委托模式,为每个面板添加点击监听
  • classList.toggle() 方法智能切换 active 类
  • 简洁高效,易于理解和维护

CSS Transition 属性深度解析

在这个项目中,我们大量使用了 CSS transition 属性来创建平滑的动画效果。让我们深入了解这个强大的特性:

transition 属性完整语法

transition: property duration timing-function delay;

属性分解说明

  1. transition-property - 指定要过渡的 CSS 属性

    transition-property: all;           /* 所有可过渡属性 */
    transition-property: opacity;       /* 仅透明度 */
    transition-property: transform, opacity; /* 多个属性 */
    
  2. transition-duration - 过渡持续时间

    transition-duration: 700ms;        /* 700毫秒 */
    transition-duration: 1s;           /* 1秒 */
    transition-duration: 0.5s;         /* 0.5秒 */
    
  3. transition-timing-function - 时间函数(动画速度曲线)

    transition-timing-function: ease-in;     /* 慢开始 */
    transition-timing-function: ease-out;    /* 慢结束 */
    transition-timing-function: ease-in-out; /* 慢开始和结束 */
    transition-timing-function: linear;      /* 匀速 */
    transition-timing-function: cubic-bezier(0.1, 0.7, 1.0, 0.1); /* 贝塞尔曲线 */
    
  4. transition-delay - 过渡延迟时间

    transition-delay: 400ms;           /* 400毫秒后开始 */
    transition-delay: 0s;              /* 立即开始(默认) */
    transition-delay: 1s;              /* 1秒后开始 */
    

项目中使用的过渡效果

/* 面板的主要过渡 */
.panel {
    transition: all 700ms ease-in;
    /* 等价于: */
    transition-property: all;
    transition-duration: 700ms;
    transition-timing-function: ease-in;
    transition-delay: 0s;
}

/* 标题的延迟过渡 */
.panel h3 {
    transition: opacity 300ms ease-in 400ms;
    /* 等价于: */
    transition-property: opacity;
    transition-duration: 300ms;
    transition-timing-function: ease-in;
    transition-delay: 400ms;
}

时间函数详解

时间函数控制动画的速度曲线,对动画的"感觉"至关重要:

  • ease-in - 慢开始,逐渐加速(项目中使用的)

    transition-timing-function: ease-in;
    

    适合元素进入场景的动画

  • ease-out - 快速开始,慢结束

    transition-timing-function: ease-out;
    

    适合元素离开场景的动画

  • ease-in-out - 慢开始和慢结束

    transition-timing-function: ease-in-out;
    

    适合需要平滑变化的动画

  • linear - 匀速运动

    transition-timing-function: linear;
    

    机械感强,适合进度条等

  • cubic-bezier - 自定义贝塞尔曲线

    transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
    

    创建弹性、反弹等特殊效果

可过渡的 CSS 属性

并非所有 CSS 属性都可以应用过渡效果,常见的可过渡属性包括:

  • 尺寸相关:width, height, flex-grow, flex-shrink
  • 位置相关:margin, padding, top, left, right, bottom
  • 颜色相关:color, background-color, border-color
  • 视觉效果:opacity, visibility, box-shadow
  • 变换相关:transform 的所有函数

多重过渡

一个元素可以同时应用多个不同的过渡效果:

.element {
    transition: 
        opacity 300ms ease-in,
        transform 500ms ease-out 100ms,
        background-color 200ms linear;
}

弹性布局(Flexbox)深度解析

什么是弹性布局?

Flexbox(弹性盒子布局)是 CSS3 中一种新的布局模式,专门为解决复杂布局而设计。它能够让我们更轻松地创建响应式布局,特别是在一维布局中表现出色。

Flexbox 核心概念

1. 弹性容器和弹性项目

.container {
    display: flex; /* 容器变为弹性容器 */
}
.panel {
    /* 每个 panel 自动成为弹性项目 */
}

2. 主轴和交叉轴

  • 主轴(main axis) :弹性项目主要沿此方向排列
  • 交叉轴(cross axis) :与主轴垂直的方向

Flexbox 容器属性详解

1. flex-direction - 定义主轴方向

.container {
    flex-direction: row;            /* 默认:水平从左到右 */
    flex-direction: row-reverse;    /* 水平从右到左 */
    flex-direction: column;         /* 垂直从上到下 */
    flex-direction: column-reverse; /* 垂直从下到上 */
}

2. justify-content - 主轴对齐方式

.container {
    justify-content: flex-start;    /* 默认:从主轴起点开始 */
    justify-content: flex-end;      /* 从主轴终点开始 */
    justify-content: center;        /* 居中对齐 */
    justify-content: space-between; /* 两端对齐,项目间间隔相等 */
    justify-content: space-around;  /* 每个项目两侧间隔相等 */
    justify-content: space-evenly;  /* 项目间和两端间隔都相等 */
}

3. align-items - 交叉轴对齐方式

.container {
    align-items: stretch;       /* 默认:拉伸填满容器高度 */
    align-items: flex-start;    /* 交叉轴起点对齐 */
    align-items: flex-end;      /* 交叉轴终点对齐 */
    align-items: center;        /* 交叉轴居中对齐 */
    align-items: baseline;      /* 项目的第一行文字基线对齐 */
}

4. flex-wrap - 换行控制

.container {
    flex-wrap: nowrap;      /* 默认:不换行 */
    flex-wrap: wrap;        /* 换行,第一行在上方 */
    flex-wrap: wrap-reverse; /* 换行,第一行在下方 */
}

Flexbox 项目属性详解

1. flex-grow - 放大比例

.panel {
    flex-grow: 0.5; /* 默认0,不放大 */
}
.panel.active {
    flex-grow: 5;   /* 放大5倍 */
}

2. flex-shrink - 缩小比例

.panel {
    flex-shrink: 1; /* 默认1,空间不足时等比例缩小 */
}

3. flex-basis - 项目初始大小

.panel {
    flex-basis: auto; /* 默认auto,基于内容计算 */
    flex-basis: 200px; /* 固定200px */
    flex-basis: 20%;   /* 容器宽度的20% */
}

简写顺序

flex: <grow> <shrink> <basis>

flex: 2 1 300px;   /* grow = 2, shrink = 1, basis = 300px */

项目中 Flexbox 的应用

.container {
    display: flex;          /* 创建弹性容器 */
    width: 90vw;           /* 容器宽度 */
    /* 默认值:flex-direction: row, justify-content: flex-start */
}

.panel {
    flex: 0.5;             /* 初始状态:flex-grow: 0.5 */
    height: 80vh;          /* 固定高度 */
}

.panel.active {
    flex: 5;               /* 激活状态:flex-grow: 5 */
}

布局计算原理

  • 初始状态:所有面板 flex-grow 总和 = 0.5 × 5 = 2.5
  • 激活面板占据:5 ÷ 2.5 = 2/5 的剩余空间
  • 其他面板各占据:0.5 ÷ 2.5 = 1/5 的剩余空间
  1. flex-grow 分配的是剩余空间,不是总空间
  2. 分配比例 = 项目flex-grow ÷ 所有项目flex-grow总和
  3. 最终宽度 = 基础宽度 + 分配的剩余空间

弹性布局的实际应用技巧

1. 居中布局的多种方式

传统方式

.center {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

Flexbox 方式

.container {
    display: flex;
    justify-content: center;  /* 水平居中 */
    align-items: center;      /* 垂直居中 */
}

2. 等分布局

.container {
    display: flex;
}
.item {
    flex: 1;  /* 所有项目等分空间 */
}

3. 圣杯布局

.container {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}
.header, .footer {
    flex: 0 0 auto;  /* 不伸缩,基于内容高度 */
}
.content {
    flex: 1;        /* 占据剩余所有空间 */
}

4. 响应式导航

.nav {
    display: flex;
    flex-wrap: wrap;
}
.nav-item {
    flex: 1 0 200px;  /* 最小200px,可伸缩 */
}

Stylus 预处理器详解

项目中提供了 Stylus 版本,展示了 CSS 预处理器的强大功能:

Stylus 是什么?

Stylus 是一种 CSS 预处理器,它扩展了 CSS 的功能,提供了更简洁、更强大的语法,最终编译成标准的 CSS。

安装和使用

# 全局安装 Stylus
npm install -g stylus

# 编译 Stylus 文件为 CSS
stylus style.styl -o style.css

# 监听文件变化自动编译
stylus style.styl -o style.css -w

Stylus 语法特性

1. 简洁的语法(可选花括号、分号和冒号)

/* Stylus 语法 */
.container
  display flex
  width 90vw
  
/* 编译后的 CSS */
.container {
  display: flex;
  width: 90vw;
}

2. 嵌套规则

.container
  display flex
  .panel
    height 80vh
    h3
      font-size 24px

3. & 符号引用父选择器

.panel
  height 80vh
  &.active
    flex 5
  &:hover
    transform scale(1.05)

4. 变量和计算

// 定义变量
primary-color = #fff
panel-height = 80vh

.panel
  color primary-color
  height panel-height
  margin panel-height * 0.1

5. Mixins(混合)

// 定义混合
border-radius(n)
  -webkit-border-radius n
  -moz-border-radius n
  border-radius n

// 使用混合
.panel
  border-radius(50px)

Stylus 优势总结

  • 代码更简洁:减少约 40% 的代码量
  • 可读性更强:清晰的嵌套结构
  • 维护更方便:变量和混合功能
  • 自动化前缀:自动添加浏览器前缀
  1. 减少重绘区域
    • 确保动画元素有自己的复合层
    • 使用 transform: translateZ(0) 开启硬件加速

总结

通过这个扩展卡片项目,我们深入学习了:

  1. Flexbox 弹性布局的完整体系:容器属性、项目属性、主轴交叉轴概念
  2. CSS Transition 的详细配置:属性、时长、时间函数、延迟的完整用法
  3. Stylus 预处理器的强大功能:更简洁、更强大的 CSS 编写方式
  4. 响应式设计的实践:通过媒体查询确保多设备兼容性

弹性布局的核心价值在于它提供了一种更加直观、灵活的布局方式,特别适合构建现代 Web 应用的界面。结合 CSS 过渡动画,我们可以创建出既美观又交互流畅的用户体验。

前端开发的进阶之路在于掌握这些基础技术的深度原理,并能够灵活运用于实际项目中。通过这个项目,你不仅学会了一个酷炫的效果,更重要的是掌握了实现这种效果的核心技术原理。


完整代码已在文章中提供,建议亲手实践并尝试不同的修改。你可以调整 flex-grow 值观察布局变化,修改 transition 参数体验不同的动画效果,尝试用 Stylus 重写 CSS 代码,添加新的交互功能或动画效果,创造出属于你自己的独特效果!

从零构建一个HTML5敲击乐Web应用:前端开发最佳实践指南

2025年10月29日 00:53

从零构建一个HTML5敲击乐Web应用:前端开发最佳实践指南

在当今Web开发领域,HTML5技术已经成为构建现代Web应用的标准。本文将带您从零开始构建一个有趣的敲击乐Web应用,同时分享前端开发中的最佳实践和关键技术要点。

项目概述:HTML5敲击乐应用

image.png 我们的目标是创建一个响应式的敲击乐应用,用户可以通过键盘按键触发不同的音效和视觉效果。这个项目虽然看似简单,但涵盖了现代前端开发的多个核心概念:

  • HTML5语义化结构
  • CSS3动画和过渡效果
  • JavaScript事件处理
  • 响应式设计原则
  • 性能优化技巧

一、HTML5页面结构设计

良好的HTML结构是Web应用的基石。我们的应用包含以下核心结构:

<div class="keys">
  <div class="key" data-key="65">
    <h3>A</h3>
    <span class="sound">clap</span>
  </div>
  <!-- 更多按键... -->
</div>

为什么使用data-*属性?

data-key属性是HTML5自定义数据属性,它允许我们在HTML元素上存储额外信息。这种方式比使用ID或类名更语义化,也更易于维护。

二、CSS Reset与全局样式

2.1 CSS Reset的必要性

不同浏览器对HTML元素有各自的默认样式,这会导致跨浏览器显示不一致的问题。CSS Reset的作用就是统一或清除这些默认样式差异,为开发提供一个一致的基础。

/* Eric Meyer's Reset CSS v3.0.0 */
html, body, div, span, /*...*/ video {
  margin: 0;
  padding: 0;
  border: 0;
  font-size: 100%;
  font: inherit;
  vertical-align: baseline;
}

2.2 现代全局样式设置

除了传统的Reset,现代前端开发还推荐以下全局设置:

*, *::before, *::after {
  box-sizing: border-box; /* 更直观的盒模型 */
}

img {
  max-width: 100%; /* 响应式图片 */
  height: auto;
  display: block; /* 避免图片下方间隙 */
}

a {
  text-decoration: none; /* 去除下划线 */
  color: inherit; /* 继承父元素颜色 */
}

三、响应式布局与Flexbox

3.1 Flexbox布局

我们的敲击乐应用使用Flexbox实现弹性布局,这是现代响应式设计的首选方案:

.keys {
  display: flex; /* 启用弹性布局 */
  min-height: 100vh; /* 视窗高度 */
  align-items: center; /* 垂直居中 */
  justify-content: center; /* 水平居中 */
}

Flexbox的优势在于:

  • 简单易用的对齐和分布方式
  • 自动处理不同屏幕尺寸下的布局
  • 无需复杂的浮动或定位代码

3.2 响应式单位

我们使用相对单位而非固定像素,确保在不同设备上都能良好显示:

html {
  font-size: 10px; /* 基准大小 */
}

.key {
  border: .4rem solid black; /* 相对于根元素字体大小 */
  margin: 1rem;
  width: 10rem;
}

推荐使用:

  • rem:相对于根元素字体大小
  • vh/vw:相对于视窗高度/宽度
  • %:相对于父元素

四、背景与视觉效果

4.1 背景图片处理

html {
  background: url('./background.jpg') bottom center no-repeat;
  background-size: cover;
}

background-size的两个重要值:

  • cover:等比例缩放覆盖整个容器,可能裁剪部分图片
  • contain:等比例缩放完整显示图片,可能留有空白

4.2 按键动画效果

通过CSS过渡和变换实现按键按下时的视觉效果:

.key {
  transition: all .07s ease; /* 平滑过渡效果 */
}

.playing {
  transform: scale(1.1); /* 放大效果 */
  border-color: orange;
  box-shadow: 0 0 1rem orange; /* 发光效果 */
}

五、JavaScript交互实现

5.1 事件监听最佳实践

document.addEventListener('DOMContentLoaded', function() {
  // 页面加载完成后执行
});

为什么使用DOMContentLoaded而不是window.onload

  • DOMContentLoaded:HTML文档完全加载和解析后触发
  • window.onload:等待所有资源(包括图片)加载完成
  • 前者能更快启动交互逻辑

5.2 按键事件处理

function playSound(event) {
  const keyCode = event.keyCode;
  const element = document.querySelector(`.key[data-key="${keyCode}"]`);
  element.classList.add('playing');
}

window.addEventListener('keydown', playSound);

5.3 性能优化技巧

  1. 脚本位置:将<script>标签放在</body>前,避免阻塞HTML解析
  2. 事件委托:对于大量相似元素,使用事件委托而非单独绑定
  3. CSS动画优于JS动画:利用浏览器硬件加速

六、完整代码实现

以下是完整的HTML结构:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>HTML敲击乐</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="keys">
    <div class="key" data-key="65">
      <h3>A</h3>
      <span class="sound">clap</span>
    </div>
    <!-- 更多按键... -->
  </div>
  
  <script src="script.js"></script>
</body>
</html>

七、扩展与优化方向

  1. 添加触摸支持:扩展touchstart事件处理移动端触摸
  2. 音效实现:使用Web Audio API添加实际音效
  3. 主题切换:通过CSS变量实现多主题支持
  4. 性能监控:使用Performance API分析关键指标
  5. PWA支持:添加Service Worker实现离线功能

结语

通过这个HTML5敲击乐应用的开发,我们实践了现代前端开发的多个核心概念:语义化HTML、模块化CSS、响应式设计、事件驱动编程等。这些技术不仅适用于小型演示项目,也是构建大型Web应用的基础。 记住,优秀的前端开发不仅仅是实现功能,更要考虑:

  • 代码的可维护性和可扩展性
  • 跨浏览器和设备兼容性
  • 用户体验和性能优化

希望本文能为您的Web开发之旅提供有价值的参考。Happy coding!

CSS3 星球大战:用前端技术打造震撼的3D动画效果

作者 San30
2025年10月28日 23:59

前言

在当今的前端开发中,CSS3 提供了强大的动画和 3D 变换能力,让我们能够创造出令人惊艳的视觉效果。今天,我将分享如何使用纯 CSS 实现一个星球大战风格的标题动画,这个效果包含了 3D 变换、关键帧动画和透视效果等高级 CSS 特性。

项目概述

这个星球大战动画效果包含三个主要元素:

  • "STAR" 文字从上方向中心移动并消失
  • "WARS" 文字从下方向中心移动并消失
  • "THE FORCE AWAKEN" 字幕从远到近旋转出现

HTML 结构设计

首先,我们来看 HTML 结构的设计思路:

<div class="starwars">
  <img src="./img/star.svg" alt="star" class="star">
  <img src="./img/wars.svg" alt="wars" class="wars">
  <h2 class="byline" id="byline">
    <span>T</span>
    <span>h</span>
    <span>e</span>
    <span>F</span>
    <span>o</span>
    <span>r</span>
    <span>c</span>
    <span>e</span>
    <span>A</span>
    <span>w</span>
    <span>a</span>
    <span>k</span>
    <span>e</span>
  </h2>
</div>

设计思路

  • 使用语义化的 HTML5 结构
  • 将 "STAR" 和 "WARS" 作为 SVG 图片引入,保证视觉效果
  • 将副标题拆分为单个字母的 <span> 元素,为每个字母单独设置动画

CSS 核心技术解析

1. 重置样式与基础设置

我们首先使用标准的 CSS Reset 来确保跨浏览器的一致性:

/* 所有元素应用 border-box 模型 */
*, *::before, *::after {
  box-sizing: border-box;
}

/* 重置所有元素的内外边距 */
html, body, div, span, /* ... 其他元素 */ {
  margin: 0;
  padding: 0;
  border: 0;
  font-size: 100%;
  font: inherit;
  vertical-align: baseline;
}

2. 容器与 3D 环境设置

创建 3D 动画环境是整个效果的关键:

.starwars {
  width: 34em;  /* 相对于字体大小的相对单位 */
  height: 17em;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%); /* 经典居中技巧 */
  perspective: 800px; /* 透视距离,模拟3D效果 */
  transform-style: preserve-3d; /* 子元素保持3D效果 */
}

关键技术点

  • perspective: 800px 创建 3D 透视效果,值越小透视效果越强
  • transform-style: preserve-3d 确保子元素在 3D 空间中正确渲染
  • 使用 transform: translate(-50%, -50%) 实现完美居中

3. "STAR" 文字动画

.star {
  top: -0.75em;
  animation: star 10s ease-out infinite;
}

@keyframes star {
  0% {
    opacity: 0;
    transform: scale(1.5) translateY(-0.75em);
  }
  20% {
    opacity: 1;
  }
  89% {
    opacity: 1;
    transform: scale(1);
  }
  100% {
    opacity: 0;
    transform: translateZ(-1000em);
  }
}

动画解析

  • 初始状态:放大 1.5 倍并向上偏移,透明度为 0
  • 20% 时完全显示
  • 89% 时保持正常大小和透明度
  • 最终向 Z 轴深处移动并消失,创造深度感

4. "WARS" 文字动画

.wars {
  bottom: -0.5em;
  animation: wars 10s ease-out infinite;
}

@keyframes wars {
  0% {
    opacity: 0;
    transform: scale(1.5) translateY(0.5em);
  }
  20% {
    opacity: 1;
  }
  90% {
    opacity: 1;
    transform: scale(1);
  }
  100% {
    opacity: 0;
    transform: translateZ(-1000em);
  }
}

与 "STAR" 动画类似,但方向相反,从下方进入。

5. 字幕字母动画

这是最复杂的效果,每个字母都有独立的旋转动画:

.byline {
  /* ... 其他样式 */
  animation: move-byline 10s linear infinite;
}

.byline span {
  display: inline-block;
  animation: spin-letter 10s linear infinite;
}

@keyframes move-byline {
  0% {
    transform: translateZ(25em); /* 从远处开始 */
  }
  100% {
    transform: translateZ(0); /* 移动到正常位置 */
  }
}

@keyframes spin-letter {
  0%, 10% {
    opacity: 0;
    transform: rotateY(90deg) translateZ(20px);
  }
  30% {
    opacity: 1;
    transform: rotateY(0) translateZ(0);
  }
  70%, 86% {
    transform: rotateY(0) translateZ(0);
    opacity: 1;
  }
  95% {
    opacity: 0;
  }
}

技术亮点

  • 每个字母绕 Y 轴旋转 90 度开始,创造翻转效果
  • 使用 translateZ 增强 3D 深度感
  • 错开的显示和隐藏时间创造流畅的序列效果

CSS Animation 属性深度解析

在这个项目中,我们大量使用了 CSS animation 属性,让我们深入了解这个强大的特性:

animation 属性完整语法

animation: name duration timing-function delay iteration-count direction fill-mode play-state;

分解说明

  1. animation-name - 动画名称

    animation-name: star; /* 对应 @keyframes star */
    
  2. animation-duration - 动画持续时间

    animation-duration: 10s; /* 10秒完成一次动画 */
    animation-duration: 500ms; /* 500毫秒完成 */
    
  3. animation-timing-function - 动画时间函数

    animation-timing-function: ease-out; /* 慢结束 */
    animation-timing-function: linear;   /* 匀速 */
    animation-timing-function: ease-in;  /* 慢开始 */
    animation-timing-function: cubic-bezier(0.1, 0.7, 1.0, 0.1); /* 贝塞尔曲线 */
    
  4. animation-delay - 动画延迟

    animation-delay: 2s; /* 2秒后开始动画 */
    
  5. animation-iteration-count - 动画重复次数

    animation-iteration-count: infinite; /* 无限循环 */
    animation-iteration-count: 3;        /* 重复3次 */
    animation-iteration-count: 1;        /* 只播放一次 */
    
  6. animation-direction - 动画方向

    animation-direction: normal;      /* 正常播放 */
    animation-direction: reverse;     /* 反向播放 */
    animation-direction: alternate;   /* 正反交替 */
    animation-direction: alternate-reverse; /* 反-正交替 */
    
  7. animation-fill-mode - 动画前后状态

    animation-fill-mode: none;        /* 默认,动画前后不应用样式 */
    animation-fill-mode: forwards;    /* 动画结束后保持最后一帧样式 */
    animation-fill-mode: backwards;   /* 动画开始前应用第一帧样式 */
    animation-fill-mode: both;        /* 同时应用forwards和backwards */
    
  8. animation-play-state - 动画播放状态

    animation-play-state: running;    /* 播放中 */
    animation-play-state: paused;     /* 暂停 */
    

项目中使用的简写形式

.star {
  animation: star 10s ease-out infinite;
}
/* 等价于 */
.star {
  animation-name: star;
  animation-duration: 10s;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
  animation-delay: 0s;
  animation-direction: normal;
  animation-fill-mode: none;
  animation-play-state: running;
}

时间函数详解

时间函数控制动画的速度曲线,对动画的"感觉"至关重要:

  • linear - 匀速运动,机械感强

    animation-timing-function: linear;
    
  • ease - 默认值,慢开始→加速→慢结束

    animation-timing-function: ease;
    
  • ease-in - 慢开始,适合出场动画

    animation-timing-function: ease-in;
    
  • ease-out - 慢结束,适合入场动画(项目中使用的)

    animation-timing-function: ease-out;
    
  • ease-in-out - 慢开始和慢结束

    animation-timing-function: ease-in-out;
    
  • cubic-bezier - 自定义贝塞尔曲线

    animation-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55); /* 弹性效果 */
    

多动画同时应用

一个元素可以同时应用多个动画:

.element {
  animation: 
    fadeIn 2s ease-in,
    slideUp 1s ease-out 2s,
    bounce 0.5s ease-in-out 3s 3;
}

CSS Transform 属性深度解析

在这个项目中,我们充分利用了 CSS transform 属性,以下是该属性的完整指南:

平移变换 (Translate)

transform: translateX(50px);    /* 水平平移 */
transform: translateY(30px);    /* 垂直平移 */
transform: translate(50px, 30px); /* 同时平移 */
transform: translateZ(100px);   /* Z轴平移(3D) */

旋转变换 (Rotate)

transform: rotate(45deg);       /* 2D旋转 */
transform: rotateY(45deg);      /* 绕Y轴旋转(3D) */
transform: rotateX(45deg);      /* 绕X轴旋转(3D) */

缩放变换 (Scale)

transform: scale(1.5);          /* 等比例缩放 */
transform: scale(1.5, 0.8);     /* 非等比例缩放 */

响应式设计考虑

虽然这个示例主要关注动画效果,但在实际项目中还需要考虑响应式设计:

@media (max-width: 768px) {
  .starwars {
    width: 90vw;
    height: 45vw;
    perspective: 400px;
  }
  
  .byline {
    font-size: 1em;
    letter-spacing: 0.2em;
  }
}

性能优化建议

  1. 使用 transformopacity 进行动画,这些属性不会触发重排
  2. 启用 GPU 加速:3D 变换会自动启用 GPU 加速
  3. 减少动画元素数量:在移动设备上可以考虑减少同时动画的元素
  4. 使用 will-change 属性提示浏览器元素将要变化:
.star, .wars, .byline span {
  will-change: transform, opacity;
}

总结

通过这个星球大战动画项目,我们深入探索了:

  1. CSS Animation 系统:完整的动画属性体系和关键帧定义
  2. CSS 3D 变换系统perspectivetransform-style 和 3D 变换函数
  3. 关键帧动画:使用 @keyframes 创建复杂动画序列
  4. 动画性能优化:选择合适的动画属性和时间函数
  5. 响应式动画设计:确保动画在不同设备上的良好表现

这些技术不仅可以用于创建电影标题效果,还可以应用于网站加载动画、交互反馈和产品展示等多种场景。掌握这些 CSS3 高级特性,将极大提升你创建现代、吸引人网页体验的能力。

前端开发者就像是代码界的导演,通过 CSS 和 JavaScript 编排元素的出场、动作和退场,创造出令人难忘的数字体验。希望这个项目能激发你探索更多 CSS 动画的可能性!


完整代码已在文章开头提供,你可以直接下载使用或根据自己的需求进行修改。尝试调整动画时间、变换参数或添加新的动画效果,创造出属于你自己的震撼视觉效果!

❌
❌