2024最新版本 electron + vite + sqlite 开发收藏夹程序【完结】

2024最新版本 electron + vite + sqlite 开发收藏夹程序
2024最新版本 electron + vite + sqlite 开发收藏夹程序【续一】

上篇 2024最新版本 electron + vite + sqlite 开发收藏夹程序【续一】

数据库设计

SQLite的一些操作基本上跟 MySQL 差不多,在这个项目中只需要简单的两张表

表一 fav_list

收藏夹 ID收藏类目 ID标题链接
fav_idclass_idfav_titlefav_url

表二 fav_class 【不考虑多级类目】

收藏类目 ID类目名称
class_idclass_name

electron/database.js

在electron目录下创建 database.js

这里偷懒了,直接 runSql 执行 SQL 语句,没有具体到 CRUD

const { app } = require("electron");
const sqlite3 = require("sqlite3");
const path = require("node:path");
const fse = require("fs-extra");
var dbPath;
var db = null;

if (app.isPackaged) {
  dbPath = path.resolve("./resources/storage/data.db");
} else {
  dbPath = path.join(__dirname, "./storage/data.db");
}
fse.ensureFileSync(dbPath);


const SQLiteInit = () => {
  db = new sqlite3.Database(dbPath, (err) => {
    if (err) throw err;
  });
};


const createTable = () => {
  return new Promise((resolve) => {    
    db.serialize(function() {
      db.run(`
      create table if not exists fav_class (
        class_id INTEGER PRIMARY KEY AUTOINCREMENT, 
        class_name text
      );
    `, (err, data) => {
      if (err) throw err;
      resolve(data);
    });
      db.serialize(function() {
        db.run(`
        create table if not exists fav_list (
          fav_id INTEGER PRIMARY KEY AUTOINCREMENT, 
          class_id INTEGER,
          fav_title text,
          fav_url text      
        );
      `, (err, data) => {
        if (err) throw err;
        resolve(data);
      });
     });
      
     });

  })

}
const runSql = (sql) => {
  return new Promise((resolve) => {
    db.all(sql, (err, data) => {
      if (err) throw err;
      resolve(data);
    });
  });
};

module.exports = {
  SQLiteInit,
  createTable,  
  runSql,
};

操作数据库

载入SQLite3并创建表

现在需要在主进程中载入数据库,渲染器进程中进行创建表的操作,我个人理解 electron/main.js 为主进程 ,src/pages/favroite/App.vue 为渲染器进程,所以对这两个文件进行修改

1. electron/main.js

const { app, BrowserWindow, ipcMain} = require('electron')
const path = require('node:path')
const isPackaged = app.isPackaged
const sq3 = require('./database')

process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'

var mainWindow 

app.whenReady().then(() => {
    createMainWindow()  
    app.on("activate", () => {
      if (BrowserWindow.getAllWindows().length === 0) createMainWindow()
    })
  })

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

/**创建主窗口 */
const createMainWindow = () => {
  mainWindow = new BrowserWindow({
    frame:false,    
    fullscreenable:false,
    fullscreen: false,
    maximizable: false,
    shadow: true,
    hasShadow: true,
    resizable: false,
    width: 880,
    height: 500,
    webPreferences:{
      nodeIntegration:true,
      contextIsolation: true,     
      preload: path.join(__dirname, 'preload.js') 
    }
    
  })
  mainWindow.on('ready-to-show', () => {
    mainWindow.show()
  })

  if (!isPackaged){
    mainWindow.loadURL("http://localhost:5173/index/index.html");
  }else{
    mainWindow.loadFile(path.resolve(__dirname, '../build/index/index.html'))
  }

  sq3.SQLiteInit()

}

/**主窗口最小化 */
ipcMain.on('minWindow', (e, data) => {
  mainWindow.minimize()
})
/**关闭主窗口 */
ipcMain.on('closeWindow', (e, data) => {
  mainWindow.close()
})

/**创建普通子窗口 */
const createOtherWindow = (data) => {  
  otherWindow = new BrowserWindow({
      height: data.height || 640,
      width: data.width || 480,
      center: true,
      frame:false,
      fullscreen:false,
      fullscreenable: false,
      closable: true,
      resizable: false,
      maximizable: false,
      webPreferences: {
        nodeIntegration: true,
        webSecurity: false,
        webviewTag: true,
        enableRemoteModule: true,
        nodeIntegrationInWorker: true,      
        nodeIntegrationInSubFrames: true,
        preload: path.join(__dirname, 'preload.js')      
      }
    
  })
  otherWindow.on('ready-to-show', () => {
    otherWindow.show()
  })

  if (!isPackaged){
    otherWindow.loadURL("http://localhost:5173/" + data.name +"/index.html");
  }else{
    otherWindow.loadFile(path.resolve(__dirname, '../build/' + data.name + '/index.html'))
  }

}

/**建立普通子窗口 */
ipcMain.on('createOtherWindow', (e, data) => {
  createOtherWindow(data)
})
/**关闭普通子窗口 */
ipcMain.on('closeOtherWindow', () => {
  otherWindow.close()
})

/**创建数据表 */
ipcMain.on('createTable', () => {
  sq3.createTable()
})

/**执行Sql */
ipcMain.on('runSql', async (e, sql) => {
  let res = await sq3.runSql(sql)  
  e.returnValue = res
})

2. src/pages/favroite/App.vue

ipcRenderer.send(‘createTable’) ,通过ipc

<script setup>
import { ref, onMounted, onBeforeMount } from "vue";

import Aside from "./components/Aside.vue";
import Content from "./components/Content.vue";
import Header from "./components/Header.vue";

const ipcRenderer = window.electron.ipcRenderer;

const classList = ref([]);
const selectedId = ref(1);
const handleClassClick = (id) => {
  selectedId.value = id;
};

const getClassList = async () => {
  classList.value = await ipcRenderer.sendSync(
    "runSql",
    "select * from fav_class"
  ).data;
};

onBeforeMount(() => {
  ipcRenderer.sendSync("createTable");
  getClassList();
});
</script>
<template>
  <t-layout>
    <t-header>
      <Header></Header>
    </t-header>
    <t-layout>
      <t-aside>
        <Aside
          :classList="classList"
          :defaultValue="selectedId"
          @class-click="handleClassClick"
        ></Aside>
      </t-aside>
      <t-content>
        <Content :selectedId="selectedId" :classList="classList"></Content>
      </t-content>
    </t-layout>
  </t-layout>
</template>

添加必要组件

新建 src/pages/favroite/Header.vue

操作窗口关闭

<script setup>
const ipcRenderer = window.electron.ipcRenderer
const closeOtherWindow = () => {
    ipcRenderer.send('closeOtherWindow')
}
</script>
<template>    
    <t-row justify="space-between" align="middle" style="padding: var(--td-size-3);background:var(--td-brand-color-7);color:var(--td-font-white-1);cursor: pointer;">
        <t-col>我的收藏</t-col>
        <t-col title="关闭" @click="closeOtherWindow"><t-icon name="close" /></t-col>
        
    </t-row>
</template>

新建 src/pages/favroite/Aside.vue

显示收藏分类

<script setup>
import { ref } from "vue";
const props = defineProps({
  classList: Object,
});
const ipcRenderer = window.electron.ipcRenderer;
const emit = defineEmits(["class-click"]);

const handleClick = (id, index) => {
  isactive.value = index;
  emit("class-click", id);
};

const isactive = ref(0);

const form = ref(null);
const formData = ref({
  class_name: "",
});
const FORM_RULES = {
  class_name: [{ required: true, message: "标题必填" }],
};

const onSubmit = ({ validateResult, firstError }) => {
  if (validateResult === true) {
    ipcRenderer.sendSync(
      "runSql",
      `INSERT INTO fav_class (class_name) VALUES('${formData._value.class_name}')`
    );
  } else {
    MessagePlugin.warning(firstError);
  }
};

const deleteClass = (id) => {
  ipcRenderer.sendSync(
    "runSql",
    `delete from fav_class where class_id = ${id}`
  );
};
</script>

<template>
  <t-list :split="true">
    <t-list-item
      v-for="(item, index) in classList"
      :key="index"
      @click="handleClick(item.class_id, index)"
      :class="{ actived: index == isactive }"
    >
      {{ item.class_name }}
      <template #action>
        <t-popconfirm
          theme="default"
          content="是否删除"
          @confirm="deleteClass(item.class_id)"
        >
          <t-icon name="delete" />
        </t-popconfirm>
      </template>
    </t-list-item>

    <template #footer>
      <t-form
        ref="form"
        :rules="FORM_RULES"
        :data="formData"
        @submit="onSubmit"
        style="height: 120px"
      >
        <t-form-item
          label="分类名称"
          name="class_name"
          label-align="top"
          layout="inline"
        >
          <t-space>
            <t-input
              style="width: 120px"
              v-model="formData.class_name"
              placeholder="请输入分类名称"
            ></t-input>
            <t-button ghost theme="primary" type="submit" variant="dashed">
              <t-icon name="add"></t-icon>
            </t-button>
          </t-space>
        </t-form-item>
      </t-form>
    </template>
  </t-list>
</template>

<style scoped lang="less">
.actived {
  background-color: var(--td-gray-color-2);
  color: var(--td-brand-color) !important;
}

.t-list-item:hover {
  cursor: pointer;
  background: var(--td-gray-color-1);
  color: var(--td-font-gray-2);
}
</style>

src/pages/favroite/Content.vue

显示收藏夹列表

<script setup>
import { ref, onMounted, computed } from "vue";
import axios from "axios";
import cheerio, { load } from "cheerio";
const ipcRenderer = window.electron.ipcRenderer;

const props = defineProps(["selectedId", "classList"]);

const fav_list = ref();
const total = ref(0);
const loading = ref(false);

const insertData = async (data) => {
  await ipcRenderer.sendSync(
    "runSql",
    `INSERT INTO fav_list (fav_url, fav_title, class_id) VALUES('${data.fav_url}', '${data.fav_title}', '${data.class_id}')`
  );
  getData();
};

const updateData = async (data) => {
  let sql =
    "update favroite set fav_type = " +
    data.fav_type +
    " where fav_id = " +
    data.fav_id;
  await ipcRenderer.sendSync("runSql", sql);
  getData();
};

const deleteData = async (data) => {
  await ipcRenderer.send(
    "runSql",
    `delete from fav_list where fav_id = ${data.fav_id}`
  );
  getData();
};

const batchDelData = async (e) => {
  if (selectedRowKeys.value.join(",").length === 0) {
    return MessagePlugin.warning("未选择任何数据!");
  }
  await ipcRenderer.send(
    "runSql",
    `delete from fav_list where fav_id in ( ${selectedRowKeys.value.join(
      ","
    )} )`
  );
  getData();
};

const getData = async () => {
  loading.value = true;
  const result = await ipcRenderer.sendSync(
    "runSql",
    `select * from fav_list where class_id = ${props.selectedId}`
  ).data;
  total.value = result.length;
  fav_list.value = result;
  const timer = setTimeout(() => {
    loading.value = false;
    clearTimeout(timer);
  }, 1000);
};

const s = computed(async () => {
  loading.value = true;
  const result = await ipcRenderer.sendSync(
    "runSql",
    `select * from fav_list where class_id = ${props.selectedId}`
  ).data;
  total.value = result.length;
  fav_list.value = result;
  const timer = setTimeout(() => {
    loading.value = false;
    clearTimeout(timer);
  }, 1000);
});

const pagination = ref({
  defaultCurrent: 1,
  defaultPageSize: 10,
  pageSizeOptions: [],
  total: total,
});

const columns = [
  { colKey: "any", type: "multiple" },
  { colKey: "fav_title", title: "标题", width: "800", ellipsis: true },
  { colKey: "tools", title: "操作" },
];

const selectedRowKeys = ref([]);

const rehandleSelectChange = (value, ctx) => {
  selectedRowKeys.value = value;
};

const go = (link) => {
  window.electron.shell.openExternal(link);
};

const visible = ref(false);

const handleClick = () => {
  issave.value = false;
  visible.value = true;
  formData.value = ref([]);
  header.value = "新增收藏";
};
const handleClose = () => {
  visible.value = false;
};

const form = ref(null);

const formData = ref({
  fav_title: "",
  fav_url: "",
  class_id: "",
});

const FORM_RULES = {
  fav_title: [{ required: true, message: "标题必填" }],
  fav_url: [{ required: true, message: "网址必填" }],
  class_id: [{ required: true, message: "类型必选" }],
};

const onReset = () => {
  MessagePlugin.success("重置成功");
};

const getTitle = async () => {
  let url = formData._value.fav_url;
  loading.value = true;
  await axios
    .get(url)
    .then((res) => {
      let $ = cheerio.load(res.data);
      formData._value.fav_title = $("title").text();
      loading.value = false;
    })
    .catch((err) => {
      MessagePlugin.warning("获取标题失败,请检查网址是否正确");
      loading.value = false;
    });
};
const onSubmit = ({ validateResult, firstError }) => {
  if (validateResult === true) {
    if (issave.value) {
      updateData({
        fav_title: formData._value.fav_title,
        fav_url: formData._value.fav_url,
        fav_type: formData._value.fav_type,
        fav_id: formData._value.fav_id,
      });
    } else {
      insertData({
        fav_title: formData._value.fav_title,
        fav_url: formData._value.fav_url,
        class_id: formData._value.class_id,
      });
    }

    MessagePlugin.success("提交成功");
    visible.value = false;
  } else {
    console.log("Validate Errors: ", firstError, validateResult);
    MessagePlugin.warning(firstError);
  }
};

const onEnter = (_, { e }) => {
  e.preventDefault();
};

const issave = ref(false);
const edit = (data) => {
  issave.value = true;
  formData._value = data;

  visible.value = true;
  header.value = "编辑收藏";
};

const header = ref("new");

onMounted(() => {});
</script>
<template>
  <t-layout class="custom">
    <t-content
      style="background: #fff; -webkit-app-region: no-drag; padding: 20px"
    >
      <t-space>
        <t-button theme="primary" @click="handleClick">
          <template #icon>
            <t-icon name="bookmark-add"></t-icon>
          </template>
          新增收藏
        </t-button>
        <t-button theme="danger" variant="base" @click="batchDelData">
          <template #icon>
            <t-icon name="delete"></t-icon>
          </template>
          批量删除
        </t-button>
      </t-space>
      <div class="mytable">
        <t-table
          v-if="s"
          :data="fav_list"
          :columns="columns"
          row-key="fav_id"
          size="small"
          :hover="true"
          :bordered="false"
          :pagination="pagination"
          @select-change="rehandleSelectChange"
          :loading="loading"
          :selected-row-keys="selectedRowKeys"
          :select-on-row-click="true"
          :resizable="false"
          lazy-load
        >
          <template #fav_title="{ row }">
            <t-row justify="space-between">
              <t-col>{{ row.fav_title }}</t-col>

              <t-col
                ><t-space>
                  <t-icon
                    class="iconbox"
                    name="edit"
                    @click="
                      edit({
                        fav_id: `${row.fav_id}`,
                        fav_url: `${row.fav_url}`,
                        fav_title: `${row.fav_title}`,
                        fav_type: `${row.fav_type}`,
                      })
                    "
                  ></t-icon>
                </t-space>
              </t-col>
            </t-row>
          </template>
          <template #tools="{ row }">
            <t-space>
              <t-button theme="success" @click="go(row.fav_url)" variant="text">
                <t-icon name="link" />
              </t-button>

              <t-popconfirm
                theme="danger"
                placement="right-bottom"
                content="是否删除"
                @confirm="deleteData(row)"
              >
                <t-button theme="danger" variant="text">
                  <t-icon name="delete" />
                </t-button>
              </t-popconfirm>
            </t-space>
          </template>
        </t-table>
      </div>
    </t-content>

    <t-drawer
      v-model:visible="visible"
      :footer="false"
      :header="header"
      placement="top"
      :on-confirm="handleClose"
      @close="handleClose"
    >
      <t-space direction="vertical" size="large" style="width: 100%">
        <t-form
          ref="form"
          :rules="FORM_RULES"
          :data="formData"
          :colon="true"
          @reset="onReset"
          @submit="onSubmit"
        >
          <t-form-item label="标题" name="fav_title">
            <t-input
              v-model="formData.fav_title"
              placeholder="请输入内容"
              @enter="onEnter"
            ></t-input>
          </t-form-item>

          <t-form-item label="网址" name="fav_url">
            <t-input
              v-model="formData.fav_url"
              placeholder="请输入内容"
              @enter="onEnter"
            ></t-input>
          </t-form-item>
          <t-form-item label="类型" name="fav_type">
            <t-select v-model="formData.class_id">
              <t-option
                v-for="(item, index) in classList"
                :key="index"
                :value="item.class_id"
                :label="item.class_name"
                >{{ item.class_name }}</t-option
              >
            </t-select>
          </t-form-item>
          <t-form-item>
            <t-space size="small">
              <t-button theme="primary" type="submit" :disabled="loading"
                >提交</t-button
              >
              <t-button theme="success" @click="getTitle" :loading="loading"
                >获取标题</t-button
              >
              <t-button theme="default" variant="base" type="reset"
                >重置</t-button
              >
            </t-space>
          </t-form-item>
        </t-form>
      </t-space>
    </t-drawer>
  </t-layout>
</template>

<style lang="less" scoped>
.mytable {
  margin-top: 10px;
}

.iconbox {
  display: none;
  cursor: pointer;
}

.t-table-td--ellipsis:hover {
  .iconbox {
    display: inline;
  }
}

.custom {
  height: 700px;
  overflow: auto;
  &::-webkit-scrollbar {
    width: 6px;
    height: 6px;
  }
  &::-webkit-scrollbar-button {
    display: none;
  }
  &::-webkit-scrollbar-track {
    background: transparent;
  }
  &::-webkit-scrollbar-track-piece {
    background-color: transparent;
  }
  &::-webkit-scrollbar-thumb {
    background: rgba(144, 147, 153, 0.3);
    cursor: pointer;
    border-radius: 4px;
  }
  &::-webkit-scrollbar-corner {
    display: none;
  }
  &::-webkit-resizer {
    display: none;
  }
}
</style>

所有功能已经写完,vscode 终端执行 yarn run electron:dev 看最终效果

在这里插入图片描述

写在最后

收藏功能虽然能用,但依然有几个地方有疑问,我目前没有办法和时间去解决,有大佬可以帮看下不?

1. 新建分类和删除分类后,不会即时刷新分类列表

2. 多个组件里面 const ipcRenderer = window.electron.ipcRenderer ,这个能不能写在一个页面里多个组件调用

水平有限,写的很烂,希望能帮助到有需要的朋友

2024.1.8 14:14

  • 18
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码小涛

您的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值